profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM

Commit

retry and error state for model catalog

commit f539f76

2 changed files with +29 and −16

Jump to a changed file
  1. src/components/debate/model-picker.tsx +13 −4
  2. 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 {