fix(site): replace model catalog loading text with skeleton (#23583)

## Changes

Replaces the "Loading model catalog..." / "Loading models..." text flash
on `/agents` with a clean skeleton loading state, and removes the
admin-nag status messages entirely.

### Removed
- `getModelCatalogStatusMessage()` function and
`modelCatalogStatusMessage` prop chain — "Loading model catalog..." /
"No chat models are configured. Ask an admin to configure one." text
below the input
- `inputStatusText` prop chain — "No models configured. Ask an admin." /
"Models are configured but unavailable. Ask an admin." inline text
- `modelCatalogError` prop from `AgentCreateForm`

### Changed
- `AgentChatInput`: when `isModelCatalogLoading` is true, renders a
`Skeleton` in place of the `ModelSelector`
- `getModelSelectorPlaceholder()`: "No Models Configured" / "No Models
Available" (title case)

### Added
- `LoadingModelCatalog` story — skeleton where model selector sits
- `NoModelsConfigured` story — selector shows "No Models Configured"

Net -69 lines.
This commit is contained in:
Kyle Carberry
2026-03-25 09:54:24 -04:00
committed by GitHub
parent f14f58a58e
commit 44baac018a
10 changed files with 52 additions and 121 deletions
@@ -89,7 +89,6 @@ const AgentCreatePage: FC = () => {
modelConfigs={chatModelConfigsQuery.data ?? []}
isModelCatalogLoading={chatModelsQuery.isLoading}
isModelConfigsLoading={chatModelConfigsQuery.isLoading}
modelCatalogError={chatModelsQuery.error}
mcpServers={mcpServersQuery.data ?? []}
onMCPAuthComplete={() => void mcpServersQuery.refetch()}
/>
+2 -17
View File
@@ -60,7 +60,6 @@ import { useGitWatcher } from "./hooks/useGitWatcher";
import {
buildModelConfigIDByModelID,
buildModelIDByConfigID,
getModelCatalogStatusMessage,
getModelOptionsFromCatalog,
getModelSelectorPlaceholder,
hasConfiguredModelsInCatalog,
@@ -345,7 +344,6 @@ const AgentDetail: FC = () => {
const modelConfigs = chatModelConfigsQuery.data ?? [];
const modelCatalog = chatModelsQuery.data;
const isModelCatalogLoading = chatModelsQuery.isLoading;
const modelCatalogError = chatModelsQuery.error;
// Subscribe to live workspace updates so that agent status changes
// (e.g. connected/disconnected) are reflected without a page refresh.
@@ -542,17 +540,6 @@ const AgentDetail: FC = () => {
isModelCatalogLoading,
hasConfiguredModels,
);
const modelCatalogStatusMessage = getModelCatalogStatusMessage(
modelCatalog,
modelOptions,
isModelCatalogLoading,
Boolean(modelCatalogError),
);
const inputStatusText = hasModelOptions
? null
: hasConfiguredModels
? "Models are configured but unavailable. Ask an admin."
: "No models configured. Ask an admin.";
const isSubmissionPending =
sendMutation.isPending ||
editMutation.isPending ||
@@ -858,8 +845,7 @@ const AgentDetail: FC = () => {
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
hasModelOptions={hasModelOptions}
inputStatusText={inputStatusText}
modelCatalogStatusMessage={modelCatalogStatusMessage}
isModelCatalogLoading={isModelCatalogLoading}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
showRightPanel={showSidebarPanel}
@@ -894,8 +880,7 @@ const AgentDetail: FC = () => {
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
hasModelOptions={hasModelOptions}
inputStatusText={inputStatusText}
modelCatalogStatusMessage={modelCatalogStatusMessage}
isModelCatalogLoading={isModelCatalogLoading}
compressionThreshold={compressionThreshold}
isInputDisabled={isInputDisabled}
isSubmissionPending={isSubmissionPending}
@@ -28,8 +28,6 @@ const meta: Meta<typeof AgentChatInput> = {
modelOptions: [...defaultModelOptions],
modelSelectorPlaceholder: "Select model",
hasModelOptions: true,
inputStatusText: null,
modelCatalogStatusMessage: null,
},
};
@@ -48,6 +48,7 @@ import {
PopoverTrigger,
} from "#/components/Popover/Popover";
import { Separator } from "#/components/Separator/Separator";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import { Spinner } from "#/components/Spinner/Spinner";
import { Switch } from "#/components/Switch/Switch";
import {
@@ -104,9 +105,7 @@ interface AgentChatInputProps {
modelOptions: readonly ModelSelectorOption[];
modelSelectorPlaceholder: string;
hasModelOptions: boolean;
// Status messages.
inputStatusText: string | null;
modelCatalogStatusMessage: string | null;
isModelCatalogLoading?: boolean;
// Streaming controls (optional, for the detail page).
isStreaming?: boolean;
onInterrupt?: () => void;
@@ -468,8 +467,7 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
modelOptions,
modelSelectorPlaceholder,
hasModelOptions,
inputStatusText,
modelCatalogStatusMessage,
isModelCatalogLoading = false,
isStreaming = false,
onInterrupt,
isInterruptPending = false,
@@ -1027,16 +1025,20 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
)}
</PopoverContent>
</Popover>
<ModelSelector
value={selectedModel}
onValueChange={onModelChange}
options={modelOptions}
disabled={isDisabled}
placeholder={modelSelectorPlaceholder}
formatProviderLabel={formatProviderLabel}
dropdownSide="top"
dropdownAlign="center"
/>
{isModelCatalogLoading ? (
<Skeleton className="h-6 w-24 rounded" />
) : (
<ModelSelector
value={selectedModel}
onValueChange={onModelChange}
options={modelOptions}
disabled={isDisabled}
placeholder={modelSelectorPlaceholder}
formatProviderLabel={formatProviderLabel}
dropdownSide="top"
dropdownAlign="center"
/>
)}
{selectedWorkspace && onWorkspaceChange && (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-surface-secondary px-2 py-0.5 text-xs font-medium text-content-secondary">
<MonitorIcon className="size-3" />
@@ -1081,11 +1083,6 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
</span>
);
})}
{inputStatusText && (
<span className="hidden text-xs text-content-secondary sm:inline">
{inputStatusText}
</span>
)}
</div>
<div className="flex items-center gap-2">
{speech.isSupported && !isStreaming && (
@@ -1158,16 +1155,6 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
)}
</div>
</div>
{inputStatusText && (
<div className="px-2.5 pb-1 text-xs text-content-secondary sm:hidden">
{inputStatusText}
</div>
)}
{modelCatalogStatusMessage && (
<div className="px-2.5 pb-1 text-2xs text-content-secondary">
{modelCatalogStatusMessage}
</div>
)}
</div>
</div>
);
@@ -27,7 +27,6 @@ const meta: Meta<typeof AgentCreateForm> = {
isModelCatalogLoading: false,
modelConfigs: [],
isModelConfigsLoading: false,
modelCatalogError: undefined,
},
beforeEach: () => {
localStorage.clear();
@@ -170,6 +169,26 @@ export const SelectWorkspaceViaSearch: Story = {
},
};
export const LoadingModelCatalog: Story = {
args: {
...defaultArgs,
modelCatalog: null,
modelOptions: [],
isModelCatalogLoading: true,
isModelConfigsLoading: true,
},
};
export const NoModelsConfigured: Story = {
args: {
...defaultArgs,
modelCatalog: { providers: [] },
modelOptions: [],
isModelCatalogLoading: false,
isModelConfigsLoading: false,
},
};
export const UsageLimitExceeded: Story = {
args: {
...defaultArgs,
@@ -12,7 +12,6 @@ import { Button } from "#/components/Button/Button";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFileAttachments } from "../hooks/useFileAttachments";
import {
getModelCatalogStatusMessage,
getModelSelectorPlaceholder,
getNormalizedModelRef,
hasConfiguredModelsInCatalog,
@@ -103,7 +102,6 @@ interface AgentCreateFormProps {
isModelCatalogLoading: boolean;
modelConfigs: readonly TypesGen.ChatModelConfig[];
isModelConfigsLoading: boolean;
modelCatalogError: unknown;
mcpServers?: readonly TypesGen.MCPServerConfig[];
onMCPAuthComplete?: (serverId: string) => void;
}
@@ -117,7 +115,6 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
modelConfigs,
isModelCatalogLoading,
isModelConfigsLoading,
modelCatalogError,
mcpServers,
onMCPAuthComplete,
}) => {
@@ -190,18 +187,6 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
isModelCatalogLoading,
hasConfiguredModels,
);
const modelCatalogStatusMessage = getModelCatalogStatusMessage(
modelCatalog,
modelOptions,
isModelCatalogLoading,
Boolean(modelCatalogError),
);
const inputStatusText = hasModelOptions
? null
: hasConfiguredModels
? "Models are configured but unavailable. Ask an admin."
: "No models configured. Ask an admin.";
useEffect(() => {
if (!initialLastModelConfigID) {
return;
@@ -350,9 +335,8 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
onModelChange={handleModelChange}
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
isModelCatalogLoading={isModelCatalogLoading}
hasModelOptions={hasModelOptions}
inputStatusText={inputStatusText}
modelCatalogStatusMessage={modelCatalogStatusMessage}
attachments={attachments}
onAttach={handleAttach}
onRemoveAttachment={handleRemoveAttachment}
@@ -200,8 +200,7 @@ interface AgentDetailInputProps {
onModelChange: (modelID: string) => void;
modelOptions: readonly ModelSelectorOption[];
modelSelectorPlaceholder: string;
inputStatusText: string | null;
modelCatalogStatusMessage: string | null;
isModelCatalogLoading?: boolean;
// Controlled input value and editing state, owned by the
// conversation component.
inputRef?: React.Ref<ChatMessageInputRef>;
@@ -240,8 +239,7 @@ export const AgentDetailInput: FC<AgentDetailInputProps> = ({
onModelChange,
modelOptions,
modelSelectorPlaceholder,
inputStatusText,
modelCatalogStatusMessage,
isModelCatalogLoading = false,
inputRef,
initialValue,
onContentChange,
@@ -392,8 +390,7 @@ export const AgentDetailInput: FC<AgentDetailInputProps> = ({
onModelChange={onModelChange}
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
inputStatusText={inputStatusText}
modelCatalogStatusMessage={modelCatalogStatusMessage}
isModelCatalogLoading={isModelCatalogLoading}
mcpServers={mcpServers}
selectedMCPServerIds={selectedMCPServerIds}
onMCPSelectionChange={onMCPSelectionChange}
@@ -111,8 +111,6 @@ const StoryAgentDetailView: FC<StoryProps> = ({ editing, ...overrides }) => {
modelOptions: defaultModelOptions,
modelSelectorPlaceholder: "Select a model",
hasModelOptions: true,
inputStatusText: null as string | null,
modelCatalogStatusMessage: null as string | null,
compressionThreshold: undefined as number | undefined,
isInputDisabled: false,
isSubmissionPending: false,
@@ -276,7 +274,6 @@ export const NoModelOptions: Story = {
<StoryAgentDetailView
hasModelOptions={false}
modelOptions={[]}
inputStatusText="No models configured. Ask an admin."
isInputDisabled
/>
),
@@ -308,8 +305,6 @@ export const Loading: Story = {
modelOptions={defaultModelOptions}
modelSelectorPlaceholder="Select a model"
hasModelOptions
inputStatusText={null}
modelCatalogStatusMessage={null}
isSidebarCollapsed={false}
onToggleSidebarCollapsed={fn()}
showRightPanel={false}
@@ -328,8 +323,6 @@ export const LoadingWithModelOptions: Story = {
modelOptions={defaultModelOptions}
modelSelectorPlaceholder="Select a model"
hasModelOptions
inputStatusText={null}
modelCatalogStatusMessage={null}
isSidebarCollapsed={false}
onToggleSidebarCollapsed={fn()}
showRightPanel={false}
@@ -347,8 +340,6 @@ export const LoadingWithRightPanel: Story = {
modelOptions={defaultModelOptions}
modelSelectorPlaceholder="Select a model"
hasModelOptions
inputStatusText={null}
modelCatalogStatusMessage={null}
isSidebarCollapsed={false}
onToggleSidebarCollapsed={fn()}
showRightPanel
@@ -367,8 +358,6 @@ export const LoadingSidebarCollapsed: Story = {
modelOptions={defaultModelOptions}
modelSelectorPlaceholder="Select a model"
hasModelOptions
inputStatusText={null}
modelCatalogStatusMessage={null}
isSidebarCollapsed
onToggleSidebarCollapsed={fn()}
showRightPanel={false}
@@ -73,8 +73,7 @@ interface AgentDetailViewProps {
modelOptions: readonly ModelSelectorOption[];
modelSelectorPlaceholder: string;
hasModelOptions: boolean;
inputStatusText: string | null;
modelCatalogStatusMessage: string | null;
isModelCatalogLoading?: boolean;
compressionThreshold: number | undefined;
isInputDisabled: boolean;
isSubmissionPending: boolean;
@@ -152,8 +151,7 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
modelOptions,
modelSelectorPlaceholder,
hasModelOptions,
inputStatusText,
modelCatalogStatusMessage,
isModelCatalogLoading = false,
compressionThreshold,
isInputDisabled,
isSubmissionPending,
@@ -302,8 +300,7 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
onModelChange={setSelectedModel}
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
inputStatusText={inputStatusText}
modelCatalogStatusMessage={modelCatalogStatusMessage}
isModelCatalogLoading={isModelCatalogLoading}
inputRef={editing.chatInputRef}
initialValue={editing.editorInitialValue}
onContentChange={editing.handleContentChange}
@@ -372,8 +369,7 @@ interface AgentDetailLoadingViewProps {
modelOptions: readonly ModelSelectorOption[];
modelSelectorPlaceholder: string;
hasModelOptions: boolean;
inputStatusText: string | null;
modelCatalogStatusMessage: string | null;
isModelCatalogLoading?: boolean;
isSidebarCollapsed: boolean;
onToggleSidebarCollapsed: () => void;
showRightPanel: boolean;
@@ -387,8 +383,7 @@ export const AgentDetailLoadingView: FC<AgentDetailLoadingViewProps> = ({
modelOptions,
modelSelectorPlaceholder,
hasModelOptions,
inputStatusText,
modelCatalogStatusMessage,
isModelCatalogLoading = false,
isSidebarCollapsed,
onToggleSidebarCollapsed,
showRightPanel,
@@ -439,11 +434,10 @@ export const AgentDetailLoadingView: FC<AgentDetailLoadingViewProps> = ({
onModelChange={setSelectedModel}
modelOptions={modelOptions}
modelSelectorPlaceholder={modelSelectorPlaceholder}
isModelCatalogLoading={isModelCatalogLoading}
hasModelOptions={hasModelOptions}
inputStatusText={inputStatusText}
modelCatalogStatusMessage={modelCatalogStatusMessage}
/>
</div>
</div>{" "}
</div>
{showRightPanel && (
<RightPanel
@@ -216,28 +216,7 @@ export const getModelSelectorPlaceholder = (
return "Loading models...";
}
if (hasConfiguredModels) {
return "No available models";
return "No Models Available";
}
return "No models configured";
};
export const getModelCatalogStatusMessage = (
catalog: TypesGen.ChatModelsResponse | null | undefined,
modelOptions: readonly ModelSelectorOption[],
isModelCatalogLoading: boolean,
hasModelCatalogError: boolean,
): string | null => {
if (modelOptions.length > 0) {
return null;
}
if (isModelCatalogLoading) {
return "Loading model catalog...";
}
if (hasModelCatalogError) {
return "Model catalog unavailable. Unable to verify model availability.";
}
if (hasConfiguredModelsInCatalog(catalog)) {
return "Models are configured but unavailable. Check provider settings.";
}
return "No chat models are configured. Ask an admin to configure one.";
return "No Models Configured";
};