Skip to content

Commit 0102774

Browse files
committed
Better handling of no models
1 parent 38ac25a commit 0102774

8 files changed

Lines changed: 81 additions & 6 deletions

File tree

ui/src/components/ChatHeader/ChatHeader.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ interface ChatHeaderProps {
4545
/** Callback when instances change */
4646
onInstancesChange: (instances: ModelInstance[]) => void;
4747
availableModels: ModelInfo[];
48+
/** Whether models are still loading */
49+
isLoadingModels?: boolean;
4850
/** Callback when instance parameters change */
4951
onInstanceParametersChange?: (instanceId: string, params: ModelParameters) => void;
5052
/** Callback when instance label changes */
@@ -81,6 +83,7 @@ export function ChatHeader({
8183
selectedInstances,
8284
onInstancesChange,
8385
availableModels,
86+
isLoadingModels = false,
8487
onInstanceParametersChange,
8588
onInstanceLabelChange,
8689
disabledInstances = [],
@@ -153,6 +156,7 @@ export function ChatHeader({
153156
selectedInstances={selectedInstances}
154157
onInstancesChange={onInstancesChange}
155158
availableModels={availableModels}
159+
isLoading={isLoadingModels}
156160
onInstanceParametersChange={onInstanceParametersChange}
157161
onInstanceLabelChange={onInstanceLabelChange}
158162
disabledInstances={disabledInstances}
@@ -428,6 +432,7 @@ export function ChatHeader({
428432
selectedInstances={selectedInstances}
429433
onInstancesChange={onInstancesChange}
430434
availableModels={availableModels}
435+
isLoading={isLoadingModels}
431436
onInstanceParametersChange={onInstanceParametersChange}
432437
onInstanceLabelChange={onInstanceLabelChange}
433438
disabledInstances={disabledInstances}

ui/src/components/ChatMessageList/ChatMessageList.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ interface MessageGroup {
103103
interface ChatMessageListProps {
104104
/** Whether to show loading state for models */
105105
isLoadingModels?: boolean;
106+
/** Whether models finished loading but none are available */
107+
noModelsAvailable?: boolean;
106108
/** Callback to regenerate a response */
107109
onRegenerate?: (messageId: string, model: string) => void;
108110
/** Callback to fork conversation from a specific message */
@@ -115,6 +117,7 @@ interface ChatMessageListProps {
115117

116118
export function ChatMessageList({
117119
isLoadingModels = false,
120+
noModelsAvailable = false,
118121
onRegenerate,
119122
onForkFromMessage,
120123
onEditAndRerun,
@@ -334,7 +337,11 @@ export function ChatMessageList({
334337
<div className={`mx-auto px-3 py-4 sm:px-4 sm:py-6 ${widescreenMode ? "" : "max-w-6xl"}`}>
335338
{!hasMessages && !hasStreamingResponses ? (
336339
<div className="h-[calc(100vh-280px)] sm:h-[calc(100vh-300px)] flex items-center justify-center">
337-
<EmptyChat selectedModels={selectedModels} isLoadingModels={isLoadingModels} />
340+
<EmptyChat
341+
selectedModels={selectedModels}
342+
isLoadingModels={isLoadingModels}
343+
noModelsAvailable={noModelsAvailable}
344+
/>
338345
</div>
339346
) : (
340347
<div

ui/src/components/ChatView/ChatView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export function ChatView({
176176
selectedInstances={selectedInstances}
177177
onInstancesChange={setSelectedInstances}
178178
availableModels={availableModels}
179+
isLoadingModels={isLoadingModels}
179180
onInstanceParametersChange={handleInstanceParametersChange}
180181
onInstanceLabelChange={handleInstanceLabelChange}
181182
disabledInstances={disabledInstances}
@@ -199,6 +200,7 @@ export function ChatView({
199200
<main className="flex flex-1 flex-col overflow-hidden">
200201
<ChatMessageList
201202
isLoadingModels={isLoadingModels}
203+
noModelsAvailable={!isLoadingModels && availableModels.length === 0}
202204
onRegenerate={onRegenerate}
203205
onRegenerateAll={onRegenerateAll}
204206
onForkFromMessage={onForkFromMessage}

ui/src/components/EmptyChat/EmptyChat.stories.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ export const LoadingModels: Story = {
112112
},
113113
};
114114

115+
export const NoModelsAvailable: Story = {
116+
args: {
117+
selectedModels: [],
118+
isLoadingModels: false,
119+
noModelsAvailable: true,
120+
},
121+
play: async ({ canvasElement }) => {
122+
const canvas = within(canvasElement);
123+
124+
// Should show no models message
125+
await expect(canvas.getByText(/no models available/i)).toBeInTheDocument();
126+
await expect(canvas.getByText(/add a provider/i)).toBeInTheDocument();
127+
128+
// Should NOT show example prompts when no models are available
129+
await expect(canvas.queryByText(/General/i)).not.toBeInTheDocument();
130+
},
131+
};
132+
115133
/**
116134
* Test: Clicking a category shows its prompts
117135
*/

ui/src/components/EmptyChat/EmptyChat.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ import { EXAMPLE_PROMPT_CATEGORIES, type PromptCategory } from "./examplePrompts
88
interface EmptyChatProps {
99
selectedModels: string[];
1010
isLoadingModels?: boolean;
11+
noModelsAvailable?: boolean;
1112
}
1213

13-
export function EmptyChat({ selectedModels, isLoadingModels = false }: EmptyChatProps) {
14+
export function EmptyChat({
15+
selectedModels,
16+
isLoadingModels = false,
17+
noModelsAvailable = false,
18+
}: EmptyChatProps) {
1419
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
1520
const setPendingPrompt = useChatUIStore((s) => s.setPendingPrompt);
1621

1722
const getMessage = () => {
23+
if (noModelsAvailable) {
24+
return "No models available. Add a provider in settings to get started.";
25+
}
1826
if (selectedModels.length === 0) {
1927
return "Select a model to start chatting.";
2028
}
@@ -49,7 +57,7 @@ export function EmptyChat({ selectedModels, isLoadingModels = false }: EmptyChat
4957
)}
5058

5159
{/* Example prompts section */}
52-
{!isLoadingModels && (
60+
{!isLoadingModels && !noModelsAvailable && (
5361
<div className="mt-8 w-full max-w-2xl">
5462
{/* Category tabs */}
5563
<div className="flex flex-wrap justify-center gap-2 mb-4">

ui/src/components/ModelSelector/ModelSelector.stories.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,35 @@ function WithDuplicateSettingsStory() {
298298
export const WithDuplicateSettings: Story = {
299299
render: () => <WithDuplicateSettingsStory />,
300300
};
301+
302+
function LoadingStory() {
303+
const [instances, setInstances] = useState<ModelInstance[]>([]);
304+
return (
305+
<ModelSelector
306+
selectedInstances={instances}
307+
onInstancesChange={setInstances}
308+
availableModels={[]}
309+
isLoading={true}
310+
/>
311+
);
312+
}
313+
314+
export const Loading: Story = {
315+
render: () => <LoadingStory />,
316+
};
317+
318+
function NoModelsAvailableStory() {
319+
const [instances, setInstances] = useState<ModelInstance[]>([]);
320+
return (
321+
<ModelSelector
322+
selectedInstances={instances}
323+
onInstancesChange={setInstances}
324+
availableModels={[]}
325+
isLoading={false}
326+
/>
327+
);
328+
}
329+
330+
export const NoModelsAvailable: Story = {
331+
render: () => <NoModelsAvailableStory />,
332+
};

ui/src/components/ModelSelector/ModelSelector.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ interface ModelSelectorProps {
4343
/** Callback when instances change */
4444
onInstancesChange: (instances: ModelInstance[]) => void;
4545
availableModels: ModelInfo[];
46+
/** Whether models are still loading from the API */
47+
isLoading?: boolean;
4648
maxModels?: number;
4749
/** Callback when instance parameters change */
4850
onInstanceParametersChange?: (instanceId: string, params: ModelParameters) => void;
@@ -188,6 +190,7 @@ export function ModelSelector({
188190
selectedInstances,
189191
onInstancesChange,
190192
availableModels,
193+
isLoading: isLoadingProp,
191194
maxModels = 10,
192195
onInstanceParametersChange,
193196
onInstanceLabelChange,
@@ -298,7 +301,7 @@ export function ModelSelector({
298301
]);
299302
};
300303

301-
const isLoading = availableModels.length === 0;
304+
const isLoading = isLoadingProp ?? false;
302305
const canToggleDisabled = hasMessages && !!onDisabledInstancesChange;
303306

304307
// Convert instances to model IDs for the picker (for backward compatibility)

ui/src/pages/chat/ChatPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default function ChatPage() {
3737
const { preferences } = usePreferences();
3838

3939
// Fetch models from API
40-
const { data: modelsResponse } = useQuery(apiV1ModelsOptions());
40+
const { data: modelsResponse, isPending: isLoadingModels } = useQuery(apiV1ModelsOptions());
4141
const availableModels: ModelInfo[] = useMemo(
4242
() => modelsResponse?.data?.map((m) => m as ModelInfo).filter((m) => m.id) || [],
4343
[modelsResponse?.data]
@@ -259,7 +259,7 @@ export default function ChatPage() {
259259
availableModels={availableModels}
260260
conversation={currentConversation}
261261
isStreaming={isStreaming}
262-
isLoadingModels={availableModels.length === 0}
262+
isLoadingModels={isLoadingModels}
263263
onSendMessage={handleSendMessage}
264264
onStopStreaming={stopStreaming}
265265
onClearMessages={clearMessages}

0 commit comments

Comments
 (0)