@@ -19,148 +19,173 @@ import {
19
19
AlertDialogCancel
20
20
} from './ui/alert-dialog' ;
21
21
import { Model , DownloadedModel , DownloadProgress } from '../constants/models' ;
22
+ import { api } from '@/trpc/react' ;
22
23
23
24
export const ModelsView : React . FC = ( ) => {
24
- const [ availableModels , setAvailableModels ] = useState < Model [ ] > ( [ ] ) ;
25
- const [ downloadedModels , setDownloadedModels ] = useState < Record < string , DownloadedModel > > ( { } ) ;
26
25
const [ downloadProgress , setDownloadProgress ] = useState < Record < string , DownloadProgress > > ( { } ) ;
27
- const [ loading , setLoading ] = useState ( true ) ;
28
- const [ isLocalWhisperAvailable , setIsLocalWhisperAvailable ] = useState ( false ) ;
29
- const [ selectedModel , setSelectedModel ] = useState < string | null > ( null ) ;
30
26
const [ showDeleteDialog , setShowDeleteDialog ] = useState ( false ) ;
31
27
const [ modelToDelete , setModelToDelete ] = useState < string | null > ( null ) ;
32
28
33
- const loadData = async ( ) => {
34
- try {
35
- setLoading ( true ) ;
36
- const [ available , downloaded , activeDownloads , whisperAvailable , currentSelectedModel ] = await Promise . all ( [
37
- window . electronAPI . getAvailableModels ( ) ,
38
- window . electronAPI . getDownloadedModels ( ) ,
39
- window . electronAPI . getActiveDownloads ( ) ,
40
- window . electronAPI . isLocalWhisperAvailable ( ) ,
41
- window . electronAPI . getSelectedModel ( ) ,
42
- ] ) ;
43
-
44
- setAvailableModels ( available ) ;
45
- setDownloadedModels ( downloaded ) ;
46
- setIsLocalWhisperAvailable ( whisperAvailable ) ;
47
- setSelectedModel ( currentSelectedModel ) ;
48
-
49
- // Set up active downloads progress
50
- const progressMap : Record < string , DownloadProgress > = { } ;
51
- for ( const downloadProgress of activeDownloads ) {
52
- progressMap [ downloadProgress . modelId ] = downloadProgress ;
29
+ // tRPC queries
30
+ const availableModelsQuery = api . models . getAvailableModels . useQuery ( ) ;
31
+ const downloadedModelsQuery = api . models . getDownloadedModels . useQuery ( ) ;
32
+ const activeDownloadsQuery = api . models . getActiveDownloads . useQuery ( ) ;
33
+ const isLocalWhisperAvailableQuery = api . models . isLocalWhisperAvailable . useQuery ( ) ;
34
+ const selectedModelQuery = api . models . getSelectedModel . useQuery ( ) ;
35
+
36
+ const utils = api . useUtils ( ) ;
37
+
38
+ // tRPC mutations
39
+ const downloadModelMutation = api . models . downloadModel . useMutation ( {
40
+ onSuccess : ( ) => {
41
+ utils . models . getDownloadedModels . invalidate ( ) ;
42
+ utils . models . getActiveDownloads . invalidate ( ) ;
43
+ } ,
44
+ onError : ( error ) => {
45
+ console . error ( 'Failed to start download:' , error ) ;
46
+ if ( error instanceof Error && error . message . includes ( 'AbortError' ) ) {
47
+ console . log ( 'Download was manually aborted, not showing error' ) ;
48
+ return ;
53
49
}
54
- setDownloadProgress ( progressMap ) ;
55
- } catch ( err ) {
56
- console . error ( 'Failed to load models data:' , err ) ;
57
- toast . error ( 'Failed to load models data' ) ;
58
- } finally {
59
- setLoading ( false ) ;
50
+ toast . error ( 'Failed to start download' ) ;
60
51
}
61
- } ;
52
+ } ) ;
62
53
54
+ const cancelDownloadMutation = api . models . cancelDownload . useMutation ( {
55
+ onSuccess : ( ) => {
56
+ utils . models . getActiveDownloads . invalidate ( ) ;
57
+ } ,
58
+ onError : ( error ) => {
59
+ console . error ( 'Failed to cancel download:' , error ) ;
60
+ toast . error ( 'Failed to cancel download' ) ;
61
+ }
62
+ } ) ;
63
+
64
+ const deleteModelMutation = api . models . deleteModel . useMutation ( {
65
+ onSuccess : ( ) => {
66
+ utils . models . getDownloadedModels . invalidate ( ) ;
67
+ setShowDeleteDialog ( false ) ;
68
+ setModelToDelete ( null ) ;
69
+ } ,
70
+ onError : ( error ) => {
71
+ console . error ( 'Failed to delete model:' , error ) ;
72
+ toast . error ( 'Failed to delete model' ) ;
73
+ setShowDeleteDialog ( false ) ;
74
+ setModelToDelete ( null ) ;
75
+ }
76
+ } ) ;
77
+
78
+ const setSelectedModelMutation = api . models . setSelectedModel . useMutation ( {
79
+ onSuccess : ( ) => {
80
+ utils . models . getSelectedModel . invalidate ( ) ;
81
+ } ,
82
+ onError : ( error ) => {
83
+ console . error ( 'Failed to select model:' , error ) ;
84
+ toast . error ( 'Failed to select model' ) ;
85
+ }
86
+ } ) ;
87
+
88
+ // Initialize active downloads progress on load
63
89
useEffect ( ( ) => {
64
- loadData ( ) ;
65
-
66
- const handleDownloadProgress = ( modelId : string , progress : DownloadProgress ) => {
67
- setDownloadProgress ( prev => ( {
68
- ...prev ,
69
- [ modelId ] : progress
70
- } ) ) ;
71
- } ;
72
-
73
- const handleDownloadComplete = ( modelId : string , downloadedModel : DownloadedModel ) => {
74
- setDownloadedModels ( prev => ( {
75
- ...prev ,
76
- [ modelId ] : downloadedModel
77
- } ) ) ;
90
+ if ( activeDownloadsQuery . data ) {
91
+ const progressMap : Record < string , DownloadProgress > = { } ;
92
+ activeDownloadsQuery . data . forEach ( ( download ) => {
93
+ progressMap [ download . modelId ] = download ;
94
+ } ) ;
95
+ setDownloadProgress ( progressMap ) ;
96
+ }
97
+ } , [ activeDownloadsQuery . data ] ) ;
98
+
99
+ // Set up tRPC subscriptions for real-time download updates
100
+ api . models . onDownloadProgress . useSubscription ( undefined , {
101
+ onData : ( { modelId, progress } ) => {
102
+ setDownloadProgress ( prev => ( { ...prev , [ modelId ] : progress } ) ) ;
103
+ } ,
104
+ onError : ( error ) => {
105
+ console . error ( 'Download progress subscription error:' , error ) ;
106
+ }
107
+ } ) ;
108
+
109
+ api . models . onDownloadComplete . useSubscription ( undefined , {
110
+ onData : ( { modelId, downloadedModel } ) => {
78
111
setDownloadProgress ( prev => {
79
- const updated = { ...prev } ;
80
- delete updated [ modelId ] ;
81
- return updated ;
112
+ const newProgress = { ...prev } ;
113
+ delete newProgress [ modelId ] ;
114
+ return newProgress ;
82
115
} ) ;
83
- } ;
84
-
85
- const handleDownloadError = ( modelId : string , errorMessage : string ) => {
86
- setDownloadProgress ( prev => ( {
87
- ...prev ,
88
- [ modelId ] : {
89
- ...prev [ modelId ] ,
90
- status : 'error' ,
91
- error : errorMessage
92
- }
93
- } ) ) ;
94
- } ;
95
-
96
- const handleDownloadCancelled = ( modelId : string ) => {
116
+ utils . models . getDownloadedModels . invalidate ( ) ;
117
+ utils . models . getActiveDownloads . invalidate ( ) ;
118
+ } ,
119
+ onError : ( error ) => {
120
+ console . error ( 'Download complete subscription error:' , error ) ;
121
+ }
122
+ } ) ;
123
+
124
+ api . models . onDownloadError . useSubscription ( undefined , {
125
+ onData : ( { modelId, error } ) => {
97
126
setDownloadProgress ( prev => {
98
- const updated = { ...prev } ;
99
- delete updated [ modelId ] ;
100
- return updated ;
127
+ const newProgress = { ...prev } ;
128
+ delete newProgress [ modelId ] ;
129
+ return newProgress ;
101
130
} ) ;
102
- } ;
131
+ toast . error ( `Download failed: ${ error } ` ) ;
132
+ utils . models . getActiveDownloads . invalidate ( ) ;
133
+ } ,
134
+ onError : ( error ) => {
135
+ console . error ( 'Download error subscription error:' , error ) ;
136
+ }
137
+ } ) ;
103
138
104
- const handleModelDeleted = ( modelId : string ) => {
105
- setDownloadedModels ( prev => {
106
- const updated = { ...prev } ;
107
- delete updated [ modelId ] ;
108
- return updated ;
139
+ api . models . onDownloadCancelled . useSubscription ( undefined , {
140
+ onData : ( { modelId } ) => {
141
+ setDownloadProgress ( prev => {
142
+ const newProgress = { ...prev } ;
143
+ delete newProgress [ modelId ] ;
144
+ return newProgress ;
109
145
} ) ;
110
- } ;
111
-
112
- // Listen to events from main process
113
- window . electronAPI . on ( 'model-download-progress' , handleDownloadProgress ) ;
114
- window . electronAPI . on ( 'model-download-complete' , handleDownloadComplete ) ;
115
- window . electronAPI . on ( 'model-download-error' , handleDownloadError ) ;
116
- window . electronAPI . on ( 'model-download-cancelled' , handleDownloadCancelled ) ;
117
- window . electronAPI . on ( 'model-deleted' , handleModelDeleted ) ;
118
-
119
- return ( ) => {
120
- // Cleanup event listeners
121
- window . electronAPI . off ( 'model-download-progress' , handleDownloadProgress ) ;
122
- window . electronAPI . off ( 'model-download-complete' , handleDownloadComplete ) ;
123
- window . electronAPI . off ( 'model-download-error' , handleDownloadError ) ;
124
- window . electronAPI . off ( 'model-download-cancelled' , handleDownloadCancelled ) ;
125
- window . electronAPI . off ( 'model-deleted' , handleModelDeleted ) ;
126
- } ;
127
- } , [ ] ) ;
146
+ utils . models . getActiveDownloads . invalidate ( ) ;
147
+ } ,
148
+ onError : ( error ) => {
149
+ console . error ( 'Download cancelled subscription error:' , error ) ;
150
+ }
151
+ } ) ;
152
+
153
+ api . models . onModelDeleted . useSubscription ( undefined , {
154
+ onData : ( { modelId } ) => {
155
+ utils . models . getDownloadedModels . invalidate ( ) ;
156
+ } ,
157
+ onError : ( error ) => {
158
+ console . error ( 'Model deleted subscription error:' , error ) ;
159
+ }
160
+ } ) ;
128
161
129
162
const handleDownload = async ( modelId : string , event ?: React . MouseEvent ) => {
130
- event ?. preventDefault ( ) ;
131
- event ?. stopPropagation ( ) ;
132
-
133
- console . log ( 'Start download clicked for:' , modelId ) ;
134
-
163
+ if ( event ) {
164
+ event . preventDefault ( ) ;
165
+ event . stopPropagation ( ) ;
166
+ }
167
+
135
168
try {
136
- console . log ( 'Downloading model:' , modelId ) ;
137
- await window . electronAPI . downloadModel ( modelId ) ;
138
- console . log ( 'Start download successful for:' , modelId ) ;
169
+ await downloadModelMutation . mutateAsync ( { modelId } ) ;
170
+ console . log ( 'Download started for:' , modelId ) ;
139
171
} catch ( err ) {
140
172
console . error ( 'Failed to start download:' , err ) ;
141
-
142
- // Don't show error for manual cancellations (AbortError)
143
- if ( err instanceof Error && err . message . includes ( 'AbortError' ) ) {
144
- console . log ( 'Download was manually aborted, not showing error' ) ;
145
- return ;
146
- }
147
-
148
- toast . error ( 'Failed to start download' ) ;
173
+ // Error is already handled by the mutation's onError
149
174
}
150
175
} ;
151
176
152
177
const handleCancelDownload = async ( modelId : string , event ?: React . MouseEvent ) => {
153
- event ?. preventDefault ( ) ;
154
- event ?. stopPropagation ( ) ;
155
-
156
- console . log ( 'Cancel download clicked for:' , modelId ) ;
157
-
178
+ if ( event ) {
179
+ event . preventDefault ( ) ;
180
+ event . stopPropagation ( ) ;
181
+ }
182
+
158
183
try {
159
- await window . electronAPI . cancelDownload ( modelId ) ;
184
+ await cancelDownloadMutation . mutateAsync ( { modelId } ) ;
160
185
console . log ( 'Cancel download successful for:' , modelId ) ;
161
186
} catch ( err ) {
162
187
console . error ( 'Failed to cancel download:' , err ) ;
163
- toast . error ( 'Failed to cancel download' ) ;
188
+ // Error is already handled by the mutation's onError
164
189
}
165
190
} ;
166
191
@@ -173,13 +198,10 @@ export const ModelsView: React.FC = () => {
173
198
if ( ! modelToDelete ) return ;
174
199
175
200
try {
176
- await window . electronAPI . deleteModel ( modelToDelete ) ;
201
+ await deleteModelMutation . mutateAsync ( { modelId : modelToDelete } ) ;
177
202
} catch ( err ) {
178
203
console . error ( 'Failed to delete model:' , err ) ;
179
- toast . error ( 'Failed to delete model' ) ;
180
- } finally {
181
- setShowDeleteDialog ( false ) ;
182
- setModelToDelete ( null ) ;
204
+ // Error is already handled by the mutation's onError
183
205
}
184
206
} ;
185
207
@@ -188,14 +210,12 @@ export const ModelsView: React.FC = () => {
188
210
setModelToDelete ( null ) ;
189
211
} ;
190
212
191
-
192
213
const handleSelectModel = async ( modelId : string ) => {
193
214
try {
194
- await window . electronAPI . setSelectedModel ( modelId ) ;
195
- setSelectedModel ( modelId ) ;
215
+ await setSelectedModelMutation . mutateAsync ( { modelId } ) ;
196
216
} catch ( err ) {
197
217
console . error ( 'Failed to select model:' , err ) ;
198
- toast . error ( 'Failed to select model' ) ;
218
+ // Error is already handled by the mutation's onError
199
219
}
200
220
} ;
201
221
@@ -207,6 +227,16 @@ export const ModelsView: React.FC = () => {
207
227
return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 1 ) ) + ' ' + sizes [ i ] ;
208
228
} ;
209
229
230
+ // Loading state
231
+ const loading = availableModelsQuery . isLoading || downloadedModelsQuery . isLoading ||
232
+ isLocalWhisperAvailableQuery . isLoading || selectedModelQuery . isLoading ;
233
+
234
+ // Data from queries
235
+ const availableModels = availableModelsQuery . data || [ ] ;
236
+ const downloadedModels = downloadedModelsQuery . data || { } ;
237
+ const isLocalWhisperAvailable = isLocalWhisperAvailableQuery . data || false ;
238
+ const selectedModel = selectedModelQuery . data ;
239
+
210
240
if ( loading ) {
211
241
return (
212
242
< div className = "flex items-center justify-center h-64" >
0 commit comments