mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(site): prevent layout shift when agent chat right panel loads (#22983)
This commit is contained in:
@@ -27,8 +27,7 @@ import {
|
||||
reactRouterOutlet,
|
||||
reactRouterParameters,
|
||||
} from "storybook-addon-remix-react-router";
|
||||
import AgentDetail from "./AgentDetail";
|
||||
import { RIGHT_PANEL_OPEN_KEY } from "./AgentDetailView";
|
||||
import AgentDetail, { RIGHT_PANEL_OPEN_KEY } from "./AgentDetail";
|
||||
import type { AgentsOutletContext } from "./AgentsPage";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -83,6 +83,9 @@ import {
|
||||
import { useFileAttachments } from "./useFileAttachments";
|
||||
import { useGitWatcher } from "./useGitWatcher";
|
||||
|
||||
/** localStorage key controlling whether the right panel is visible. */
|
||||
export const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open";
|
||||
|
||||
const localHosts = new Set(["localhost", "127.0.0.1", "0.0.0.0"]);
|
||||
|
||||
const lastModelConfigIDStorageKey = "agents.last-model-config-id";
|
||||
@@ -576,6 +579,26 @@ const AgentDetail: FC = () => {
|
||||
const chatInputRef = useRef<ChatMessageInputRef | null>(null);
|
||||
const inputValueRef = useRef("");
|
||||
|
||||
// Right panel open/closed state is owned here so the loading
|
||||
// skeleton and the loaded view share the same layout, preventing
|
||||
// a horizontal shift when data arrives.
|
||||
const [showSidebarPanel, setShowSidebarPanel] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return localStorage.getItem(RIGHT_PANEL_OPEN_KEY) === "true";
|
||||
});
|
||||
const handleSetShowSidebarPanel = useCallback(
|
||||
(next: boolean | ((prev: boolean) => boolean)) => {
|
||||
setShowSidebarPanel((prev) => {
|
||||
const value = typeof next === "function" ? next(prev) : next;
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(RIGHT_PANEL_OPEN_KEY, String(value));
|
||||
}
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const chatQuery = useQuery({
|
||||
...chat(agentId ?? ""),
|
||||
enabled: Boolean(agentId),
|
||||
@@ -1065,6 +1088,7 @@ const AgentDetail: FC = () => {
|
||||
modelCatalogStatusMessage={modelCatalogStatusMessage}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
showRightPanel={showSidebarPanel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1103,6 +1127,8 @@ const AgentDetail: FC = () => {
|
||||
isInterruptPending={interruptMutation.isPending}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
showSidebarPanel={showSidebarPanel}
|
||||
onSetShowSidebarPanel={handleSetShowSidebarPanel}
|
||||
prNumber={prNumber}
|
||||
diffStatusData={diffStatusQuery.data}
|
||||
gitWatcher={gitWatcher}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
AgentDetailLoadingView,
|
||||
AgentDetailNotFoundView,
|
||||
AgentDetailView,
|
||||
RIGHT_PANEL_OPEN_KEY,
|
||||
} from "./AgentDetailView";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -119,6 +118,8 @@ const meta: Meta<typeof AgentDetailView> = {
|
||||
isInterruptPending: false,
|
||||
isSidebarCollapsed: false,
|
||||
onToggleSidebarCollapsed: fn(),
|
||||
showSidebarPanel: false,
|
||||
onSetShowSidebarPanel: fn(),
|
||||
prNumber: undefined,
|
||||
diffStatusData: undefined,
|
||||
gitWatcher: defaultGitWatcher,
|
||||
@@ -192,11 +193,8 @@ export const SubmissionPending: Story = {
|
||||
|
||||
/** Right sidebar panel is open with diff status data. */
|
||||
export const WithSidebarPanel: Story = {
|
||||
beforeEach: () => {
|
||||
localStorage.setItem(RIGHT_PANEL_OPEN_KEY, "true");
|
||||
return () => localStorage.removeItem(RIGHT_PANEL_OPEN_KEY);
|
||||
},
|
||||
args: {
|
||||
showSidebarPanel: true,
|
||||
prNumber: 123,
|
||||
diffStatusData: {
|
||||
chat_id: AGENT_ID,
|
||||
@@ -254,6 +252,7 @@ export const Loading: Story = {
|
||||
modelCatalogStatusMessage={null}
|
||||
isSidebarCollapsed={false}
|
||||
onToggleSidebarCollapsed={fn()}
|
||||
showRightPanel={false}
|
||||
/>
|
||||
),
|
||||
};
|
||||
@@ -273,6 +272,26 @@ export const LoadingWithModelOptions: Story = {
|
||||
modelCatalogStatusMessage={null}
|
||||
isSidebarCollapsed={false}
|
||||
onToggleSidebarCollapsed={fn()}
|
||||
showRightPanel={false}
|
||||
/>
|
||||
),
|
||||
};
|
||||
/** Loading state with the right panel pre-opened. */
|
||||
export const LoadingWithRightPanel: Story = {
|
||||
render: () => (
|
||||
<AgentDetailLoadingView
|
||||
titleElement={<title>Loading — Agents</title>}
|
||||
isInputDisabled
|
||||
effectiveSelectedModel="openai:gpt-4o"
|
||||
setSelectedModel={fn()}
|
||||
modelOptions={defaultModelOptions}
|
||||
modelSelectorPlaceholder="Select a model"
|
||||
hasModelOptions
|
||||
inputStatusText={null}
|
||||
modelCatalogStatusMessage={null}
|
||||
isSidebarCollapsed={false}
|
||||
onToggleSidebarCollapsed={fn()}
|
||||
showRightPanel
|
||||
/>
|
||||
),
|
||||
};
|
||||
@@ -292,6 +311,7 @@ export const LoadingSidebarCollapsed: Story = {
|
||||
modelCatalogStatusMessage={null}
|
||||
isSidebarCollapsed
|
||||
onToggleSidebarCollapsed={fn()}
|
||||
showRightPanel={false}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ChatDiffStatusResponse } from "api/api";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import type { ModelSelectorOption } from "components/ai-elements";
|
||||
import { Skeleton } from "components/Skeleton/Skeleton";
|
||||
import { ArchiveIcon } from "lucide-react";
|
||||
import { type FC, type RefObject, useCallback, useMemo, useState } from "react";
|
||||
import { type FC, type RefObject, useMemo, useState } from "react";
|
||||
import type { UrlTransform } from "streamdown";
|
||||
import { cn } from "utils/cn";
|
||||
import { pageTitle } from "utils/page";
|
||||
@@ -11,6 +10,10 @@ import { AgentChatInput, type ChatMessageInputRef } from "./AgentChatInput";
|
||||
import { AgentDetailInput, AgentDetailTimeline } from "./AgentDetail";
|
||||
import type { useChatStore } from "./AgentDetail/ChatContext";
|
||||
import { AgentDetailTopBar } from "./AgentDetail/TopBar";
|
||||
import {
|
||||
ChatConversationSkeleton,
|
||||
RightPanelSkeleton,
|
||||
} from "./AgentsSkeletons";
|
||||
import { GitPanel } from "./GitPanel";
|
||||
import { RightPanel } from "./RightPanel";
|
||||
import { SidebarTabView } from "./SidebarTabView";
|
||||
@@ -79,6 +82,11 @@ interface AgentDetailViewProps {
|
||||
isSidebarCollapsed: boolean;
|
||||
onToggleSidebarCollapsed: () => void;
|
||||
|
||||
// Right panel state (owned by the parent so loading and
|
||||
// loaded views share the same layout).
|
||||
showSidebarPanel: boolean;
|
||||
onSetShowSidebarPanel: (next: boolean | ((prev: boolean) => boolean)) => void;
|
||||
|
||||
// Sidebar content data.
|
||||
prNumber: number | undefined;
|
||||
diffStatusData: ChatDiffStatusResponse | undefined;
|
||||
@@ -115,9 +123,6 @@ interface AgentDetailViewProps {
|
||||
urlTransform?: UrlTransform;
|
||||
}
|
||||
|
||||
/** localStorage key controlling whether the right panel is visible. */
|
||||
export const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open";
|
||||
|
||||
export const AgentDetailView: FC<AgentDetailViewProps> = ({
|
||||
agentId,
|
||||
chatTitle,
|
||||
@@ -142,6 +147,8 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
|
||||
isInterruptPending,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed,
|
||||
showSidebarPanel,
|
||||
onSetShowSidebarPanel,
|
||||
prNumber,
|
||||
diffStatusData,
|
||||
gitWatcher,
|
||||
@@ -162,25 +169,6 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
|
||||
scrollContainerRef,
|
||||
urlTransform,
|
||||
}) => {
|
||||
// Panel/sidebar UI state – purely visual, no data-fetching
|
||||
// implications. The open/closed state is persisted to localStorage
|
||||
// so users get a consistent layout when switching between chats.
|
||||
const [showSidebarPanel, setShowSidebarPanel] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return localStorage.getItem(RIGHT_PANEL_OPEN_KEY) === "true";
|
||||
});
|
||||
const handleSetShowSidebarPanel = useCallback(
|
||||
(next: boolean | ((prev: boolean) => boolean)) => {
|
||||
setShowSidebarPanel((prev) => {
|
||||
const value = typeof next === "function" ? next(prev) : next;
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(RIGHT_PANEL_OPEN_KEY, String(value));
|
||||
}
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
const [isRightPanelExpanded, setIsRightPanelExpanded] = useState(false);
|
||||
const [dragVisualExpanded, setDragVisualExpanded] = useState<boolean | null>(
|
||||
null,
|
||||
@@ -234,7 +222,7 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
|
||||
onOpenParentChat={(chatId) => onNavigateToChat(chatId)}
|
||||
panel={{
|
||||
showSidebarPanel,
|
||||
onToggleSidebar: () => handleSetShowSidebarPanel((prev) => !prev),
|
||||
onToggleSidebar: () => onSetShowSidebarPanel((prev) => !prev),
|
||||
}}
|
||||
workspace={{
|
||||
canOpenEditors,
|
||||
@@ -321,7 +309,7 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
|
||||
isOpen={shouldShowSidebar}
|
||||
isExpanded={isRightPanelExpanded}
|
||||
onToggleExpanded={() => setIsRightPanelExpanded((prev) => !prev)}
|
||||
onClose={() => handleSetShowSidebarPanel(false)}
|
||||
onClose={() => onSetShowSidebarPanel(false)}
|
||||
onVisualExpandedChange={setDragVisualExpanded}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
@@ -349,7 +337,7 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
|
||||
),
|
||||
},
|
||||
]}
|
||||
onClose={() => handleSetShowSidebarPanel(false)}
|
||||
onClose={() => onSetShowSidebarPanel(false)}
|
||||
isExpanded={visualExpanded}
|
||||
onToggleExpanded={() => setIsRightPanelExpanded((prev) => !prev)}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
@@ -373,6 +361,7 @@ interface AgentDetailLoadingViewProps {
|
||||
modelCatalogStatusMessage: string | null;
|
||||
isSidebarCollapsed: boolean;
|
||||
onToggleSidebarCollapsed: () => void;
|
||||
showRightPanel: boolean;
|
||||
}
|
||||
|
||||
export const AgentDetailLoadingView: FC<AgentDetailLoadingViewProps> = ({
|
||||
@@ -387,76 +376,73 @@ export const AgentDetailLoadingView: FC<AgentDetailLoadingViewProps> = ({
|
||||
modelCatalogStatusMessage,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed,
|
||||
showRightPanel,
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative flex h-full min-h-0 min-w-0 flex-1 flex-col">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex h-full min-h-0 min-w-0 flex-1",
|
||||
showRightPanel && "flex-row",
|
||||
)}
|
||||
>
|
||||
{titleElement}
|
||||
<AgentDetailTopBar
|
||||
panel={{
|
||||
showSidebarPanel: false,
|
||||
onToggleSidebar: () => {},
|
||||
}}
|
||||
workspace={{
|
||||
canOpenEditors: false,
|
||||
canOpenWorkspace: false,
|
||||
onOpenInEditor: () => {},
|
||||
onViewWorkspace: () => {},
|
||||
onOpenTerminal: () => {},
|
||||
sshCommand: undefined,
|
||||
}}
|
||||
onOpenParentChat={() => {}}
|
||||
onArchiveAgent={() => {}}
|
||||
onUnarchiveAgent={() => {}}
|
||||
onArchiveAndDeleteWorkspace={() => {}}
|
||||
hasWorkspace={false}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1 flex-col-reverse overflow-hidden">
|
||||
<div className="px-4">
|
||||
<div className="mx-auto w-full max-w-3xl py-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* User message bubble (right-aligned) */}
|
||||
<div className="flex w-full justify-end">
|
||||
<Skeleton className="h-10 w-2/3 rounded-lg" />
|
||||
</div>
|
||||
{/* Assistant response lines (left-aligned) */}
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<Skeleton className="h-4 w-4/6" />
|
||||
</div>
|
||||
{/* Second user message bubble */}
|
||||
<div className="mt-3 flex w-full justify-end">
|
||||
<Skeleton className="h-10 w-1/2 rounded-lg" />
|
||||
</div>
|
||||
{/* Second assistant response */}
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<Skeleton className="h-4 w-4/6" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</div>{" "}
|
||||
<div className="relative flex h-full min-h-0 min-w-0 flex-1 flex-col">
|
||||
<AgentDetailTopBar
|
||||
panel={{
|
||||
showSidebarPanel: false,
|
||||
onToggleSidebar: () => {},
|
||||
}}
|
||||
workspace={{
|
||||
canOpenEditors: false,
|
||||
canOpenWorkspace: false,
|
||||
onOpenInEditor: () => {},
|
||||
onViewWorkspace: () => {},
|
||||
onOpenTerminal: () => {},
|
||||
sshCommand: undefined,
|
||||
}}
|
||||
onOpenParentChat={() => {}}
|
||||
onArchiveAgent={() => {}}
|
||||
onUnarchiveAgent={() => {}}
|
||||
onArchiveAndDeleteWorkspace={() => {}}
|
||||
hasWorkspace={false}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1 flex-col-reverse overflow-hidden">
|
||||
<div className="px-4">
|
||||
<div className="mx-auto w-full max-w-3xl py-6">
|
||||
<ChatConversationSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 px-4">
|
||||
<AgentChatInput
|
||||
onSend={() => {}}
|
||||
initialValue=""
|
||||
isDisabled={isInputDisabled}
|
||||
isLoading={false}
|
||||
selectedModel={effectiveSelectedModel}
|
||||
onModelChange={setSelectedModel}
|
||||
modelOptions={modelOptions}
|
||||
modelSelectorPlaceholder={modelSelectorPlaceholder}
|
||||
hasModelOptions={hasModelOptions}
|
||||
inputStatusText={inputStatusText}
|
||||
modelCatalogStatusMessage={modelCatalogStatusMessage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 px-4">
|
||||
<AgentChatInput
|
||||
onSend={() => {}}
|
||||
initialValue=""
|
||||
isDisabled={isInputDisabled}
|
||||
isLoading={false}
|
||||
selectedModel={effectiveSelectedModel}
|
||||
onModelChange={setSelectedModel}
|
||||
modelOptions={modelOptions}
|
||||
modelSelectorPlaceholder={modelSelectorPlaceholder}
|
||||
hasModelOptions={hasModelOptions}
|
||||
inputStatusText={inputStatusText}
|
||||
modelCatalogStatusMessage={modelCatalogStatusMessage}
|
||||
/>
|
||||
</div>
|
||||
{showRightPanel && (
|
||||
<RightPanel
|
||||
isOpen
|
||||
isExpanded={false}
|
||||
onToggleExpanded={() => {}}
|
||||
onClose={() => {}}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
>
|
||||
<RightPanelSkeleton />
|
||||
</RightPanel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
import { Skeleton } from "components/Skeleton/Skeleton";
|
||||
import type { FC } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
/** localStorage keys shared with the agents panel components. */
|
||||
const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open";
|
||||
const RIGHT_PANEL_WIDTH_KEY = "agents.right-panel-width";
|
||||
const DEFAULT_PANEL_WIDTH = 480;
|
||||
const MIN_PANEL_WIDTH = 360;
|
||||
|
||||
/** Read persisted right-panel state for use in static skeletons. */
|
||||
function getRightPanelState(): { open: boolean; width: number } {
|
||||
if (typeof window === "undefined") {
|
||||
return { open: false, width: DEFAULT_PANEL_WIDTH };
|
||||
}
|
||||
const open = localStorage.getItem(RIGHT_PANEL_OPEN_KEY) === "true";
|
||||
const stored = localStorage.getItem(RIGHT_PANEL_WIDTH_KEY);
|
||||
let width = DEFAULT_PANEL_WIDTH;
|
||||
if (stored) {
|
||||
const parsed = Number.parseInt(stored, 10);
|
||||
if (!Number.isNaN(parsed) && parsed >= MIN_PANEL_WIDTH) {
|
||||
width = parsed;
|
||||
}
|
||||
}
|
||||
return { open, width };
|
||||
}
|
||||
|
||||
/**
|
||||
* Skeleton shown while the AgentsPage chunk is loading. Mimics the
|
||||
@@ -43,49 +67,99 @@ export const AgentsPageSkeleton: FC = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Skeleton placeholder for a chat conversation: two user message
|
||||
* bubbles interleaved with assistant response lines.
|
||||
*/
|
||||
export const ChatConversationSkeleton: FC = () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* User message bubble (right-aligned) */}
|
||||
<div className="flex w-full justify-end">
|
||||
<Skeleton className="h-10 w-2/3 rounded-lg" />
|
||||
</div>
|
||||
{/* Assistant response lines (left-aligned) */}
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<Skeleton className="h-4 w-4/6" />
|
||||
</div>
|
||||
{/* Second user message bubble */}
|
||||
<div className="mt-3 flex w-full justify-end">
|
||||
<Skeleton className="h-10 w-1/2 rounded-lg" />
|
||||
</div>
|
||||
{/* Second assistant response */}
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<Skeleton className="h-4 w-4/6" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Skeleton placeholder for the right sidebar panel: a tab bar and
|
||||
* a few content lines.
|
||||
*/
|
||||
export const RightPanelSkeleton: FC = () => (
|
||||
<div className="flex h-full min-w-0 flex-col overflow-hidden bg-surface-primary">
|
||||
{/* Skeleton tab bar */}
|
||||
<div className="flex shrink-0 items-center gap-2 border-0 border-b border-solid border-border-default px-3 py-1.5">
|
||||
<Skeleton className="h-6 w-12 rounded-md" />
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
{/* Skeleton panel content */}
|
||||
<div className="space-y-4 p-4">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Skeleton shown while the AgentDetail chunk is loading. Mimics a
|
||||
* top bar + chat conversation layout so the user sees navigable
|
||||
* structure during the brief Suspense fallback.
|
||||
*/
|
||||
export const AgentDetailSkeleton: FC = () => (
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
|
||||
{/* Minimal skeleton top bar */}
|
||||
<div className="flex shrink-0 items-center gap-2 px-4 py-2">
|
||||
<Skeleton className="h-7 w-7 rounded" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<div className="flex-1" />
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
<div className="flex h-full flex-col-reverse overflow-hidden">
|
||||
<div className="px-4">
|
||||
<div className="mx-auto w-full max-w-3xl py-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* User message bubble (right-aligned) */}
|
||||
<div className="flex w-full justify-end">
|
||||
<Skeleton className="h-10 w-2/3 rounded-lg" />
|
||||
</div>
|
||||
{/* Assistant response lines (left-aligned) */}
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<Skeleton className="h-4 w-4/6" />
|
||||
</div>
|
||||
{/* Second user message bubble */}
|
||||
<div className="mt-3 flex w-full justify-end">
|
||||
<Skeleton className="h-10 w-1/2 rounded-lg" />
|
||||
</div>
|
||||
{/* Second assistant response */}
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<Skeleton className="h-4 w-4/6" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
export const AgentDetailSkeleton: FC = () => {
|
||||
const rightPanel = getRightPanelState();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex h-full min-h-0 min-w-0 flex-1",
|
||||
rightPanel.open && "flex-row",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
|
||||
<div className="flex shrink-0 items-center gap-2 px-4 py-2">
|
||||
<Skeleton className="h-7 w-7 rounded" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<div className="flex-1" />
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
<div className="flex h-full flex-col-reverse overflow-hidden">
|
||||
<div className="px-4">
|
||||
<div className="mx-auto w-full max-w-3xl py-6">
|
||||
<ChatConversationSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rightPanel.open && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--panel-width": `${rightPanel.width}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="relative flex h-full w-[100vw] min-w-0 flex-col border-0 border-l border-solid border-border-default sm:w-[var(--panel-width)] sm:min-w-[360px] sm:max-w-[70vw]"
|
||||
>
|
||||
<RightPanelSkeleton />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user