Skip to content

Commit fc3bb71

Browse files
UI styling / code improvements (#307)
Clean up and improve UI styling * fix: UI - dependency cleanup * chore: UI - start script * refactor: UI - Extract Header * fix: UI - Header styling * fix: UI - LogViewer styling * fix: UI - Models styling * fix: UI - Activity styling * fix: UI - ConnectionStatus colors * review: UI - table border colors
1 parent c36986f commit fc3bb71

File tree

9 files changed

+262
-153
lines changed

9 files changed

+262
-153
lines changed

ui/package-lock.json

Lines changed: 108 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,29 @@
44
"version": "0.0.0",
55
"type": "module",
66
"scripts": {
7-
"dev": "vite",
7+
"start": "vite",
88
"build": "tsc -b && vite build --emptyOutDir",
99
"lint": "eslint .",
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13-
"@tailwindcss/vite": "^4.1.8",
14-
"@tanstack/react-query": "^5.80.6",
1513
"react": "^19.1.0",
1614
"react-dom": "^19.1.0",
1715
"react-icons": "^5.5.0",
1816
"react-resizable-panels": "^3.0.4",
19-
"react-router-dom": "^7.6.2",
20-
"tailwindcss": "^4.1.8"
17+
"react-router-dom": "^7.6.2"
2118
},
2219
"devDependencies": {
2320
"@eslint/js": "^9.25.0",
21+
"@tailwindcss/vite": "^4.1.8",
2422
"@types/react": "^19.1.2",
2523
"@types/react-dom": "^19.1.2",
2624
"@vitejs/plugin-react": "^4.4.1",
2725
"eslint": "^9.25.0",
2826
"eslint-plugin-react-hooks": "^5.2.0",
2927
"eslint-plugin-react-refresh": "^0.4.19",
3028
"globals": "^16.0.0",
29+
"tailwindcss": "^4.1.8",
3130
"typescript": "~5.8.3",
3231
"typescript-eslint": "^8.30.1",
3332
"vite": "^6.3.5"

ui/src/App.tsx

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
1-
import { useEffect, useCallback } from "react";
2-
import { BrowserRouter as Router, Routes, Route, Navigate, NavLink } from "react-router-dom";
3-
import { useTheme } from "./contexts/ThemeProvider";
1+
import { useEffect } from "react";
2+
import { Navigate, Route, BrowserRouter as Router, Routes } from "react-router-dom";
3+
import { Header } from "./components/Header";
44
import { useAPI } from "./contexts/APIProvider";
5+
import { useTheme } from "./contexts/ThemeProvider";
6+
import ActivityPage from "./pages/Activity";
57
import LogViewerPage from "./pages/LogViewer";
68
import ModelPage from "./pages/Models";
7-
import ActivityPage from "./pages/Activity";
8-
import ConnectionStatusIcon from "./components/ConnectionStatus";
9-
import { RiSunFill, RiMoonFill } from "react-icons/ri";
109

1110
function App() {
12-
const { isNarrow, toggleTheme, isDarkMode, appTitle, setAppTitle, setConnectionState } = useTheme();
13-
const handleTitleChange = useCallback(
14-
(newTitle: string) => {
15-
setAppTitle(newTitle.replace(/\n/g, "").trim().substring(0, 64) || "llama-swap");
16-
},
17-
[setAppTitle]
18-
);
11+
const { setConnectionState } = useTheme();
1912

2013
const { connectionStatus } = useAPI();
2114

@@ -27,42 +20,7 @@ function App() {
2720
return (
2821
<Router basename="/ui/">
2922
<div className="flex flex-col h-screen">
30-
<nav className="bg-surface border-b border-border p-2 h-[75px]">
31-
<div className="flex items-center justify-between mx-auto px-4 h-full">
32-
{!isNarrow && (
33-
<h1
34-
contentEditable
35-
suppressContentEditableWarning
36-
className="flex items-center p-0 outline-none hover:bg-gray-100 dark:hover:bg-gray-700 rounded px-1"
37-
onBlur={(e) => handleTitleChange(e.currentTarget.textContent || "(set title)")}
38-
onKeyDown={(e) => {
39-
if (e.key === "Enter") {
40-
e.preventDefault();
41-
handleTitleChange(e.currentTarget.textContent || "(set title)");
42-
e.currentTarget.blur();
43-
}
44-
}}
45-
>
46-
{appTitle}
47-
</h1>
48-
)}
49-
<div className="flex items-center space-x-4">
50-
<NavLink to="/" className={({ isActive }) => (isActive ? "navlink active" : "navlink")}>
51-
Logs
52-
</NavLink>
53-
<NavLink to="/models" className={({ isActive }) => (isActive ? "navlink active" : "navlink")}>
54-
Models
55-
</NavLink>
56-
<NavLink to="/activity" className={({ isActive }) => (isActive ? "navlink active" : "navlink")}>
57-
Activity
58-
</NavLink>
59-
<button className="" onClick={toggleTheme}>
60-
{isDarkMode ? <RiMoonFill /> : <RiSunFill />}
61-
</button>
62-
<ConnectionStatusIcon />
63-
</div>
64-
</div>
65-
</nav>
23+
<Header />
6624

6725
<main className="flex-1 overflow-auto p-4">
6826
<Routes>

ui/src/components/ConnectionStatus.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ const ConnectionStatusIcon = () => {
77
const eventStatusColor = useMemo(() => {
88
switch (connectionStatus) {
99
case "connected":
10-
return "bg-green-500";
10+
return "bg-emerald-500";
1111
case "connecting":
12-
return "bg-yellow-500";
12+
return "bg-amber-500";
1313
case "disconnected":
1414
default:
1515
return "bg-red-500";

ui/src/components/Header.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useCallback } from "react";
2+
import { RiMoonFill, RiSunFill } from "react-icons/ri";
3+
import { NavLink, type NavLinkRenderProps } from "react-router-dom";
4+
import { useTheme } from "../contexts/ThemeProvider";
5+
import ConnectionStatusIcon from "./ConnectionStatus";
6+
7+
export function Header() {
8+
const { screenWidth, toggleTheme, isDarkMode, appTitle, setAppTitle } = useTheme();
9+
const handleTitleChange = useCallback(
10+
(newTitle: string) => {
11+
setAppTitle(newTitle.replace(/\n/g, "").trim().substring(0, 64) || "llama-swap");
12+
},
13+
[setAppTitle]
14+
);
15+
16+
const navLinkClass = ({ isActive }: NavLinkRenderProps) =>
17+
`text-gray-600 hover:text-black dark:text-gray-300 dark:hover:text-gray-100 p-1 ${isActive ? "font-semibold" : ""}`;
18+
19+
return (
20+
<header className="flex items-center justify-between bg-surface border-b border-border p-2 px-4 h-[75px]">
21+
{screenWidth !== "xs" && screenWidth !== "sm" && (
22+
<h1
23+
contentEditable
24+
suppressContentEditableWarning
25+
className="p-0 outline-none hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
26+
onBlur={(e) => handleTitleChange(e.currentTarget.textContent || "(set title)")}
27+
onKeyDown={(e) => {
28+
if (e.key === "Enter") {
29+
e.preventDefault();
30+
handleTitleChange(e.currentTarget.textContent || "(set title)");
31+
e.currentTarget.blur();
32+
}
33+
}}
34+
>
35+
{appTitle}
36+
</h1>
37+
)}
38+
39+
<menu className="flex items-center gap-4">
40+
<NavLink to="/" className={navLinkClass} type="button">
41+
Logs
42+
</NavLink>
43+
<NavLink to="/models" className={navLinkClass} type="button">
44+
Models
45+
</NavLink>
46+
<NavLink to="/activity" className={navLinkClass} type="button">
47+
Activity
48+
</NavLink>
49+
<button className="" onClick={toggleTheme}>
50+
{isDarkMode ? <RiMoonFill /> : <RiSunFill />}
51+
</button>
52+
<ConnectionStatusIcon />
53+
</menu>
54+
</header>
55+
);
56+
}

ui/src/index.css

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@
9393
@apply px-4;
9494
}
9595

96+
/* Tables */
97+
table th {
98+
@apply p-2 font-semibold;
99+
}
100+
table td {
101+
@apply p-2;
102+
}
103+
96104
/* Navigation Header */
97105

98106
.navlink {
@@ -122,7 +130,7 @@
122130

123131
/* Status Badges */
124132
.status {
125-
@apply inline-block px-2 py-1 text-xs font-medium rounded-full;
133+
@apply inline-block px-2 py-1 text-xs font-medium rounded-lg;
126134
}
127135

128136
.status--ready {
@@ -140,7 +148,7 @@
140148

141149
/* Buttons */
142150
.btn {
143-
@apply bg-surface p-2 px-4 text-sm rounded-full border border-2 transition-colors duration-200 border-btn-border;
151+
@apply bg-surface py-2 px-4 text-sm rounded-md border transition-colors duration-200 border-btn-border;
144152
}
145153

146154
.btn:hover {

ui/src/pages/Activity.tsx

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,47 +43,46 @@ const ActivityPage = () => {
4343
}, [metrics]);
4444

4545
return (
46-
<div className="p-6">
47-
<h1 className="text-2xl font-bold mb-4">Activity</h1>
46+
<div className="p-2">
47+
<h1 className="text-2xl font-bold">Activity</h1>
4848

49-
{metrics.length === 0 ? (
49+
{metrics.length === 0 && (
5050
<div className="text-center py-8">
5151
<p className="text-gray-600">No metrics data available</p>
5252
</div>
53-
) : (
54-
<div className="overflow-x-auto">
53+
)}
54+
{metrics.length > 0 && (
55+
<div className="card overflow-auto">
5556
<table className="min-w-full divide-y">
56-
<thead>
57-
<tr>
58-
<th className="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider">ID</th>
59-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Time</th>
60-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Model</th>
61-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
57+
<thead className="border-gray-200 dark:border-white/10">
58+
<tr className="text-left text-xs uppercase tracking-wider">
59+
<th className="px-6 py-3">ID</th>
60+
<th className="px-6 py-3">Time</th>
61+
<th className="px-6 py-3">Model</th>
62+
<th className="px-6 py-3">
6263
Cached <Tooltip content="prompt tokens from cache" />
6364
</th>
64-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
65+
<th className="px-6 py-3">
6566
Prompt <Tooltip content="new prompt tokens processed" />
6667
</th>
67-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Generated</th>
68-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Prompt Processing</th>
69-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Generation Speed</th>
70-
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Duration</th>
68+
<th className="px-6 py-3">Generated</th>
69+
<th className="px-6 py-3">Prompt Processing</th>
70+
<th className="px-6 py-3">Generation Speed</th>
71+
<th className="px-6 py-3">Duration</th>
7172
</tr>
7273
</thead>
7374
<tbody className="divide-y">
7475
{sortedMetrics.map((metric) => (
75-
<tr key={`metric_${metric.id}`}>
76-
<td className="px-4 py-4 whitespace-nowrap text-sm">{metric.id + 1 /* un-zero index */}</td>
77-
<td className="px-6 py-4 whitespace-nowrap text-sm">{formatRelativeTime(metric.timestamp)}</td>
78-
<td className="px-6 py-4 whitespace-nowrap text-sm">{metric.model}</td>
79-
<td className="px-6 py-4 whitespace-nowrap text-sm">
80-
{metric.cache_tokens > 0 ? metric.cache_tokens.toLocaleString() : "-"}
81-
</td>
82-
<td className="px-6 py-4 whitespace-nowrap text-sm">{metric.input_tokens.toLocaleString()}</td>
83-
<td className="px-6 py-4 whitespace-nowrap text-sm">{metric.output_tokens.toLocaleString()}</td>
84-
<td className="px-6 py-4 whitespace-nowrap text-sm">{formatSpeed(metric.prompt_per_second)}</td>
85-
<td className="px-6 py-4 whitespace-nowrap text-sm">{formatSpeed(metric.tokens_per_second)}</td>
86-
<td className="px-6 py-4 whitespace-nowrap text-sm">{formatDuration(metric.duration_ms)}</td>
76+
<tr key={metric.id} className="whitespace-nowrap text-sm border-gray-200 dark:border-white/10">
77+
<td className="px-4 py-4">{metric.id + 1 /* un-zero index */}</td>
78+
<td className="px-6 py-4">{formatRelativeTime(metric.timestamp)}</td>
79+
<td className="px-6 py-4">{metric.model}</td>
80+
<td className="px-6 py-4">{metric.cache_tokens > 0 ? metric.cache_tokens.toLocaleString() : "-"}</td>
81+
<td className="px-6 py-4">{metric.input_tokens.toLocaleString()}</td>
82+
<td className="px-6 py-4">{metric.output_tokens.toLocaleString()}</td>
83+
<td className="px-6 py-4">{formatSpeed(metric.prompt_per_second)}</td>
84+
<td className="px-6 py-4">{formatSpeed(metric.tokens_per_second)}</td>
85+
<td className="px-6 py-4">{formatDuration(metric.duration_ms)}</td>
8786
</tr>
8887
))}
8988
</tbody>

ui/src/pages/LogViewer.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { useTheme } from "../contexts/ThemeProvider";
1414

1515
const LogViewer = () => {
1616
const { proxyLogs, upstreamLogs } = useAPI();
17-
const { isNarrow } = useTheme();
18-
const direction = isNarrow ? "vertical" : "horizontal";
17+
const { screenWidth } = useTheme();
18+
const direction = screenWidth === "xs" || screenWidth === "sm" ? "vertical" : "horizontal";
1919

2020
return (
2121
<PanelGroup direction={direction} className="gap-2" autoSaveId="logviewer-panel-group">
@@ -115,19 +115,19 @@ export const LogPanel = ({ id, title, logData }: LogPanelProps) => {
115115
}, [filteredLogs]);
116116

117117
return (
118-
<div className="bg-surface border border-border rounded-lg overflow-hidden flex flex-col h-full">
119-
<div className="p-4 border-b border-border bg-secondary">
118+
<div className="rounded-lg overflow-hidden flex flex-col bg-gray-950/5 dark:bg-white/10 h-full p-1">
119+
<div className="p-4">
120120
<div className="flex items-center justify-between">
121121
<h3 className="m-0 text-lg p-0">{title}</h3>
122122

123123
<div className="flex gap-2 items-center">
124-
<button className="btn" onClick={toggleFontSize}>
124+
<button className="btn border-0" onClick={toggleFontSize}>
125125
<RiFontSize />
126126
</button>
127-
<button className="btn" onClick={toggleWrapText}>
127+
<button className="btn border-0" onClick={toggleWrapText}>
128128
{wrapText ? <RiTextWrap /> : <RiAlignJustify />}
129129
</button>
130-
<button className="btn" onClick={toggleFilter}>
130+
<button className="btn border-0" onClick={toggleFilter}>
131131
{showFilter ? <RiMenuSearchFill /> : <RiMenuSearchLine />}
132132
</button>
133133
</div>
@@ -139,7 +139,7 @@ export const LogPanel = ({ id, title, logData }: LogPanelProps) => {
139139
<div className="flex gap-2 items-center w-full">
140140
<input
141141
type="text"
142-
className="w-full text-sm border p-2 rounded"
142+
className="w-full text-sm border border-gray-950/10 dark:border-white/5 p-2 rounded outline-none"
143143
placeholder="Filter logs..."
144144
value={filterRegex}
145145
onChange={(e) => setFilterRegex(e.target.value)}
@@ -151,7 +151,7 @@ export const LogPanel = ({ id, title, logData }: LogPanelProps) => {
151151
</div>
152152
)}
153153
</div>
154-
<div className="bg-background font-mono text-sm flex-1 overflow-hidden">
154+
<div className="rounded-lg bg-background font-mono text-sm flex-1 overflow-hidden">
155155
<pre ref={preTagRef} className={`${textWrapClass} ${fontSizeClass} h-full overflow-auto p-4`}>
156156
{filteredLogs}
157157
</pre>

0 commit comments

Comments
 (0)