"use client"
import { cn } from "../../utils/cn"
interface ChatMessageSkeletonProps {
className?: string
showAvatar?: boolean
isUser?: boolean
/** How many body lines to draw under the name row. Default 3 (paragraph-ish
* reply). Pass 1 for surfaces whose real messages are single-line (e.g. the
* short scripted hero-demo turns), so the skeleton isn't ~3x too tall. */
bodyLines?: number
}
// Body-line width palette: full-width lines then a short trailing line, so any
// `bodyLines` count ends on a short line like a real wrapped message.
const BODY_LINE_WIDTHS = ["w-full", "w-11/12", "w-4/5", "w-5/6"]
const bodyLineWidth = (index: number, total: number) =>
index === total - 1 ? "w-3/5" : BODY_LINE_WIDTHS[index % BODY_LINE_WIDTHS.length]
/**
* One message-row skeleton. Mirrors `ChatMessageEnhanced`'s real layout:
* full panel width, no card background, and an INLINE avatar in the name row
* (the 2025 chat pattern — Claude.ai / ChatGPT / Gemini), NOT the legacy
* `absolute -left-16` hanging avatar. Body lines use percentage widths so they
* reflow cleanly as the panel resizes.
*/
export function ChatMessageSkeleton({
className,
showAvatar = true,
isUser = false,
bodyLines = 3,
}: ChatMessageSkeletonProps) {
const lines = Math.max(1, bodyLines)
return (
{/* Name row — inline avatar (assistant only) + name + timestamp,
matching ChatMessageEnhanced's inline-avatar header. Neutral
`bg-ods-border` so the placeholders are actually visible (a tinted
low-alpha fill reads as blank on the dark surface). */}
{showAvatar && !isUser && (
)}
{/* Body lines — percentage widths so they reflow on panel resize. */}
{Array.from({ length: lines }, (_, i) => (
))}
)
}
interface ChatMessageListSkeletonProps {
className?: string
messageCount?: number
showAvatars?: boolean
/**
* Match `ChatMessageList`'s `fullWidth`: drop the centered
* `max-w-ods-content-narrow` column so the skeleton fills the panel and
* reflows on resize (instead of staying a fixed narrow column with large
* side gutters). Same semantics as the real list.
*/
fullWidth?: boolean
/** Extra classes on the content column (e.g. host-provided padding). The
* host wrapper is expected to own the panel padding; this stays empty by
* default so the skeleton sits flush. */
contentClassName?: string
/** Body lines per message row (forwarded to `ChatMessageSkeleton`). Default 3;
* pass 1 for single-line message surfaces. */
bodyLines?: number
/**
* FILL mode: top-anchor the rows and CLIP overflow (instead of bottom-anchoring
* + inner scroll). Combined with a generous `messageCount`, the rows cover the
* container top-to-bottom at ANY height — no per-container measuring, no empty
* band at the top. Use for loading surfaces that should read as "the whole
* panel is loading" rather than "a short thread pinned to the bottom".
*/
fill?: boolean
}
export function ChatMessageListSkeleton({
className,
messageCount = 6,
showAvatars = true,
fullWidth = false,
contentClassName,
bodyLines,
fill = false,
}: ChatMessageListSkeletonProps) {
const messages = Array.from({ length: messageCount }, (_, index) => ({
id: index,
isUser: index % 3 === 0,
}))
return (
{/* Bottom-anchor the rows like the real list — unless FILL mode wants
them top-anchored + clipped to cover the whole panel. */}
{!fill &&
}
{messages.map((message) => (
))}
)
}