11import { ModelCard } from "@/components/ModelCard" ;
22import { ScrollArea } from "@/components/ui/scroll-area" ;
33import { 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
67interface 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