mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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()}
|
||||
/>
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user