Skip to content

Commit 53f8837

Browse files
committed
feat: redesign Models page with improved UI/UX
- Modernize header with consistent styling across all pages - Remove icon from header for consistency - Add status indicators showing installed/available counts - Display active model with amber highlight or 'No model selected' - Enhance legend with colored icons for better visual hierarchy - Separate installed and available models into categorized sections - Improve ModelCard with minimal styling enhancements - Replace X icon with 'Remove' button for clarity - Add refined progress indicators and verifying states
1 parent 2d77331 commit 53f8837

File tree

2 files changed

+167
-60
lines changed

2 files changed

+167
-60
lines changed

src/components/ModelCard.tsx

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CheckCircle, Download, HardDrive, Loader2, Star, X, Zap } from 'lucide-react';
1+
import { CheckCircle, Download, HardDrive, Loader2, Star, X, Zap, Trash2 } from 'lucide-react';
22
import { ModelInfo } from '../types';
33
import { Button } from './ui/button';
44
import { Card } from './ui/card';
@@ -42,34 +42,34 @@ export const ModelCard = function ModelCard({
4242

4343
return (
4444
<Card
45-
className={`px-4 py-2 transition-all ${
46-
isUsable ? 'cursor-pointer' : ''
45+
className={`px-4 py-3 transition-all hover:shadow-sm ${
46+
isUsable ? 'cursor-pointer hover:border-border' : ''
4747
} ${
48-
isSelected ? 'border-primary bg-primary/5' : ''
48+
isSelected ? 'bg-primary/5 border-border/50' : 'border-border/50'
4949
}`}
5050
onClick={() => isUsable && showSelectButton && onSelect(name)}
5151
>
5252
<div className="flex items-center justify-between gap-3">
5353
{/* Model Name */}
54-
<div className="flex items-center gap-1.5 flex-shrink-0">
55-
<h3 className="font-medium">{model.display_name || name}</h3>
54+
<div className="flex items-center gap-2 flex-shrink-0 min-w-0">
55+
<h3 className="font-medium text-sm">{model.display_name || name}</h3>
5656
{model.recommended && (
57-
<Star className="w-4 h-4 fill-yellow-500 text-yellow-500" aria-label="Recommended model" />
57+
<Star className="w-3.5 h-3.5 fill-yellow-500 text-yellow-500" aria-label="Recommended model" />
5858
)}
5959
</div>
6060

6161
{/* Centered Stats */}
6262
<div className="flex items-center justify-center gap-6 flex-1">
6363
<div className="flex items-center gap-1.5">
64-
<Zap className="w-4 h-4 text-muted-foreground" />
64+
<Zap className="w-3.5 h-3.5 text-green-500/70" />
6565
<span className="text-sm font-medium">{model.speed_score}</span>
6666
</div>
6767
<div className="flex items-center gap-1.5">
68-
<CheckCircle className="w-4 h-4 text-muted-foreground" />
68+
<CheckCircle className="w-3.5 h-3.5 text-blue-500/70" />
6969
<span className="text-sm font-medium">{model.accuracy_score}</span>
7070
</div>
7171
<div className="flex items-center gap-1.5">
72-
<HardDrive className="w-4 h-4 text-muted-foreground" />
72+
<HardDrive className="w-3.5 h-3.5 text-purple-500/70" />
7373
<span className="text-sm font-medium">{formatSize()}</span>
7474
</div>
7575
</div>
@@ -86,22 +86,23 @@ export const ModelCard = function ModelCard({
8686
onDelete(name);
8787
}}
8888
variant="ghost"
89-
size="icon"
90-
className="h-8 w-8 text-muted-foreground hover:text-destructive"
89+
size="sm"
90+
className="text-muted-foreground hover:text-destructive"
9191
>
92-
<X className="w-4 h-4" />
92+
<Trash2 className="w-3.5 h-3.5 mr-1" />
93+
Remove
9394
</Button>
9495
)}
9596
</>
9697
) : isVerifying ? (
97-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
98-
<Loader2 className="w-4 h-4 animate-spin" />
99-
Verifying...
98+
<div className="flex items-center gap-2 px-2 py-1 rounded bg-yellow-500/10">
99+
<Loader2 className="w-3.5 h-3.5 animate-spin text-yellow-600" />
100+
<span className="text-xs font-medium text-yellow-600">Verifying</span>
100101
</div>
101102
) : downloadProgress !== undefined ? (
102103
<>
103-
<Progress value={downloadProgress} className="w-24 h-2" />
104-
<span className="text-sm font-medium w-10 text-right">{Math.round(downloadProgress)}%</span>
104+
<Progress value={downloadProgress} className="w-20 h-1.5" />
105+
<span className="text-xs font-medium text-blue-600 w-10 text-right">{Math.round(downloadProgress)}%</span>
105106
{onCancelDownload && (
106107
<Button
107108
onClick={(e) => {
Lines changed: 148 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { ModelCard } from "@/components/ModelCard";
22
import { ScrollArea } from "@/components/ui/scroll-area";
33
import { ModelInfo } from "@/types";
4-
import { CheckCircle, HardDrive, Star, Zap } from "lucide-react";
4+
import { CheckCircle, HardDrive, Star, Zap, Bot, Download } from "lucide-react";
5+
import { useMemo } from "react";
56

67
interface ModelsSectionProps {
78
models: [string, ModelInfo][];
@@ -21,62 +22,167 @@ export function ModelsSection({
2122
currentModel,
2223
onDownload,
2324
onDelete,
24-
onCancelDownload,
25+
onCancelDownload,
2526
onSelect
2627
}: ModelsSectionProps) {
28+
// Categorize models
29+
const { installedModels, availableModels } = useMemo(() => {
30+
const installed: [string, ModelInfo][] = [];
31+
const available: [string, ModelInfo][] = [];
32+
33+
models.forEach(([name, model]) => {
34+
if (model.downloaded) {
35+
installed.push([name, model]);
36+
} else {
37+
available.push([name, model]);
38+
}
39+
});
40+
41+
return { installedModels: installed, availableModels: available };
42+
}, [models]);
43+
44+
const hasDownloading = Object.keys(downloadProgress).length > 0;
45+
const hasVerifying = verifyingModels.size > 0;
46+
2747
return (
28-
<div className="h-full flex flex-col p-6">
29-
<div className="flex-shrink-0 mb-4 space-y-3">
30-
<h2 className="text-lg font-semibold">Models</h2>
31-
<p className="text-sm text-muted-foreground">
32-
Choose a model to transcribe your voice into text
33-
</p>
48+
<div className="h-full flex flex-col">
49+
{/* Header */}
50+
<div className="px-6 py-4 border-b border-border/40">
51+
<div className="flex items-center justify-between">
52+
<div>
53+
<h1 className="text-2xl font-semibold">Models</h1>
54+
<p className="text-sm text-muted-foreground mt-1">
55+
{installedModels.length} installed • {availableModels.length} available
56+
</p>
57+
</div>
58+
<div className="flex items-center gap-3">
59+
{(hasDownloading || hasVerifying) && (
60+
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-blue-500/10 text-sm font-medium">
61+
<Download className="h-3.5 w-3.5 text-blue-500" />
62+
{hasDownloading ? 'Downloading...' : 'Verifying...'}
63+
</div>
64+
)}
65+
{currentModel ? (
66+
<span className="text-sm text-muted-foreground">
67+
Active: <span className="text-amber-600 dark:text-amber-500">{installedModels.find(([name]) => name === currentModel)?.[1].display_name || currentModel}</span>
68+
</span>
69+
) : installedModels.length > 0 && (
70+
<span className="text-sm text-amber-600 dark:text-amber-500">
71+
No model selected
72+
</span>
73+
)}
74+
</div>
75+
</div>
76+
</div>
3477

35-
{/* Icon Legend */}
36-
<div className="flex gap-4 text-xs text-muted-foreground">
37-
<span className="flex items-center gap-1">
38-
<Zap className="w-4 h-4" />
78+
{/* Legend */}
79+
<div className="px-6 py-3 border-b border-border/20">
80+
<div className="flex items-center gap-6 text-xs text-muted-foreground">
81+
<span className="flex items-center gap-1.5">
82+
<Zap className="w-3.5 h-3.5 text-green-500" />
3983
Speed
4084
</span>
41-
<span className="flex items-center gap-1">
42-
<CheckCircle className="w-4 h-4" />
85+
<span className="flex items-center gap-1.5">
86+
<CheckCircle className="w-3.5 h-3.5 text-blue-500" />
4387
Accuracy
4488
</span>
45-
<span className="flex items-center gap-1">
46-
<HardDrive className="w-4 h-4" />
89+
<span className="flex items-center gap-1.5">
90+
<HardDrive className="w-3.5 h-3.5 text-purple-500" />
4791
Size
4892
</span>
49-
<span className="flex items-center gap-1">
50-
<Star className="w-4 h-4 fill-yellow-500 text-yellow-500" />
93+
<span className="flex items-center gap-1.5">
94+
<Star className="w-3.5 h-3.5 fill-yellow-500 text-yellow-500" />
5195
Recommended
5296
</span>
5397
</div>
5498
</div>
5599

56-
<ScrollArea className="flex-1 min-h-0">
57-
<div className="space-y-3">
58-
{models.map(([name, model]) => (
59-
<ModelCard
60-
key={name}
61-
name={name}
62-
model={model}
63-
downloadProgress={downloadProgress[name]}
64-
isVerifying={verifyingModels.has(name)}
65-
onDownload={onDownload}
66-
onDelete={onDelete}
67-
onCancelDownload={onCancelDownload}
68-
onSelect={async (modelName) => {
69-
// Only allow selection if model is downloaded
70-
if (model.downloaded) {
71-
onSelect(modelName);
72-
}
73-
}}
74-
showSelectButton={model.downloaded}
75-
isSelected={currentModel === name}
76-
/>
77-
))}
78-
</div>
79-
</ScrollArea>
100+
<div className="flex-1 min-h-0 overflow-hidden">
101+
<ScrollArea className="h-full">
102+
<div className="p-6 space-y-6">
103+
{/* Installed Models */}
104+
{installedModels.length > 0 && (
105+
<div className="space-y-4">
106+
<div className="flex items-center gap-2">
107+
<h2 className="text-base font-semibold text-foreground">Installed Models</h2>
108+
<div className="h-px bg-border/50 flex-1" />
109+
<span className="text-xs text-muted-foreground px-2 py-1 bg-muted/50 rounded">
110+
{installedModels.length}
111+
</span>
112+
</div>
113+
<div className="grid gap-3">
114+
{installedModels.map(([name, model]) => (
115+
<ModelCard
116+
key={name}
117+
name={name}
118+
model={model}
119+
downloadProgress={downloadProgress[name]}
120+
isVerifying={verifyingModels.has(name)}
121+
onDownload={onDownload}
122+
onDelete={onDelete}
123+
onCancelDownload={onCancelDownload}
124+
onSelect={async (modelName) => {
125+
if (model.downloaded) {
126+
onSelect(modelName);
127+
}
128+
}}
129+
showSelectButton={model.downloaded}
130+
isSelected={currentModel === name}
131+
/>
132+
))}
133+
</div>
134+
</div>
135+
)}
136+
137+
{/* Available Models */}
138+
{availableModels.length > 0 && (
139+
<div className="space-y-4">
140+
<div className="flex items-center gap-2">
141+
<h2 className="text-base font-semibold text-foreground">Available to Download</h2>
142+
<div className="h-px bg-border/50 flex-1" />
143+
<span className="text-xs text-muted-foreground px-2 py-1 bg-muted/50 rounded">
144+
{availableModels.length}
145+
</span>
146+
</div>
147+
<div className="grid gap-3">
148+
{availableModels.map(([name, model]) => (
149+
<ModelCard
150+
key={name}
151+
name={name}
152+
model={model}
153+
downloadProgress={downloadProgress[name]}
154+
isVerifying={verifyingModels.has(name)}
155+
onDownload={onDownload}
156+
onDelete={onDelete}
157+
onCancelDownload={onCancelDownload}
158+
onSelect={async (modelName) => {
159+
if (model.downloaded) {
160+
onSelect(modelName);
161+
}
162+
}}
163+
showSelectButton={model.downloaded}
164+
isSelected={currentModel === name}
165+
/>
166+
))}
167+
</div>
168+
</div>
169+
)}
170+
171+
{/* Empty State */}
172+
{models.length === 0 && (
173+
<div className="flex-1 flex items-center justify-center py-12">
174+
<div className="text-center">
175+
<Bot className="w-12 h-12 text-muted-foreground/30 mx-auto mb-4" />
176+
<p className="text-sm text-muted-foreground">No models available</p>
177+
<p className="text-xs text-muted-foreground/70 mt-2">
178+
Models will appear here when they become available
179+
</p>
180+
</div>
181+
</div>
182+
)}
183+
</div>
184+
</ScrollArea>
185+
</div>
80186
</div>
81187
);
82188
}

0 commit comments

Comments
 (0)