Commit
retry and error state for model catalog
commit
f539f76
2 changed files with +29 and −16
Jump to a changed file
- src/components/debate/model-picker.tsx +13 −4
- src/hooks/use-models.ts +16 −12
modified src/components/debate/model-picker.tsx +13 −4
| @@ -1,9 +1,10 @@ | ||
| 1 | 1 | 'use client'; |
| 2 | 2 | |
| 3 | -import { Plus, Search, X } from 'lucide-react'; | |
| 3 | +import { Plus, RotateCcw, Search, X } from 'lucide-react'; | |
| 4 | 4 | import { useMemo, useState } from 'react'; |
| 5 | 5 | import { filterModels } from '@/components/debate/model-combobox'; |
| 6 | 6 | import { Badge } from '@/components/ui/badge'; |
| 7 | +import { Button } from '@/components/ui/button'; | |
| 7 | 8 | import { Input } from '@/components/ui/input'; |
| 8 | 9 | import { displayNameForModel } from '@/core/types'; |
| 9 | 10 | import { useModels, pricePerMillion } from '@/hooks/use-models'; |
| @@ -19,7 +20,7 @@export function ModelPicker({ | ||
| 19 | 20 | onChange: (ids: string[]) => void; |
| 20 | 21 | max?: number; |
| 21 | 22 | }) { |
| 22 | - const { models, loading } = useModels(); | |
| 23 | + const { models, loading, error, reload } = useModels(); | |
| 23 | 24 | const [query, setQuery] = useState(''); |
| 24 | 25 | |
| 25 | 26 | const filtered = useMemo(() => filterModels(models, query, council), [models, query, council]); |
| @@ -81,8 +82,16 @@export function ModelPicker({ | ||
| 81 | 82 | </span> |
| 82 | 83 | </button> |
| 83 | 84 | ))} |
| 84 | - {!loading && filtered.length === 0 && ( | |
| 85 | - <div className="p-3 text-sm text-muted-foreground">No models match.</div> | |
| 85 | + {error && models.length === 0 ? ( | |
| 86 | + <div className="flex items-center justify-between gap-2 p-3 text-sm"> | |
| 87 | + <span className="text-muted-foreground">Could not load the model catalog.</span> | |
| 88 | + <Button variant="outline" size="sm" onClick={reload}> | |
| 89 | + <RotateCcw className="h-3.5 w-3.5" /> Retry | |
| 90 | + </Button> | |
| 91 | + </div> | |
| 92 | + ) : ( | |
| 93 | + !loading && | |
| 94 | + filtered.length === 0 && <div className="p-3 text-sm text-muted-foreground">No models match.</div> | |
| 86 | 95 | )} |
| 87 | 96 | </div> |
| 88 | 97 | </div> |
modified src/hooks/use-models.ts +16 −12
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | 'use client'; |
| 2 | 2 | |
| 3 | -import { useEffect, useState } from 'react'; | |
| 3 | +import { useCallback, useEffect, useState } from 'react'; | |
| 4 | 4 | |
| 5 | 5 | export interface ModelInfo { |
| 6 | 6 | id: string; |
| @@ -16,25 +16,29 @@let cache: ModelInfo[] | null = null; | ||
| 16 | 16 | export function useModels() { |
| 17 | 17 | const [models, setModels] = useState<ModelInfo[]>(cache ?? []); |
| 18 | 18 | const [loading, setLoading] = useState(!cache); |
| 19 | + const [error, setError] = useState<string | null>(null); | |
| 19 | 20 | |
| 20 | - useEffect(() => { | |
| 21 | - if (cache) return; | |
| 22 | - let alive = true; | |
| 21 | + const load = useCallback(() => { | |
| 22 | + setLoading(true); | |
| 23 | + setError(null); | |
| 23 | 24 | fetch('/api/models') |
| 24 | - .then((r) => r.json()) | |
| 25 | + .then((r) => { | |
| 26 | + if (!r.ok) throw new Error(`Catalog request failed (${r.status})`); | |
| 27 | + return r.json(); | |
| 28 | + }) | |
| 25 | 29 | .then((data: { models: ModelInfo[] }) => { |
| 26 | - if (!alive) return; | |
| 27 | 30 | cache = data.models; |
| 28 | 31 | setModels(data.models); |
| 29 | 32 | }) |
| 30 | - .catch(() => {}) | |
| 31 | - .finally(() => alive && setLoading(false)); | |
| 32 | - return () => { | |
| 33 | - alive = false; | |
| 34 | - }; | |
| 33 | + .catch((e: unknown) => setError(e instanceof Error ? e.message : 'Failed to load models')) | |
| 34 | + .finally(() => setLoading(false)); | |
| 35 | 35 | }, []); |
| 36 | 36 | |
| 37 | - return { models, loading }; | |
| 37 | + useEffect(() => { | |
| 38 | + if (!cache) load(); | |
| 39 | + }, [load]); | |
| 40 | + | |
| 41 | + return { models, loading, error, reload: load }; | |
| 38 | 42 | } |
| 39 | 43 | |
| 40 | 44 | export function pricePerMillion(perToken: number): string { |