A message shows its shape, and its contents when asked
A record or an artifact reference was serialised into the port row, and the panel widened until the type selects and the buttons beside them were pushed off its edge — a checkpoint reference is 130 characters of digest, and none of them are what you want while wiring a flow. What shows now is what the value *is*: 'artifact · weights.json · 60B', 'record · 3 fields'. A chevron unfolds the whole of it, wrapped, inside the panel it belongs to. A scalar still reads as itself, and scrolls its own overflow into view when it is longer than the room it was given. That behaviour already existed inside the edge inspector; it moves to Common/Marquee so the panel can have it too, and the inspector drops its own copy of the raw-JSON block along with it. The rows are smaller for it: the type select finally fits the word 'artifact', and a port nothing has come through on says so with a dash rather than a sentence — nine ports of 'nothing has come through yet' is a panel of prose about the absence of values. The e2e check asserts both halves: that the summary is what appears, and that the panel is still exactly 400px with the value unfolded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { ChevronRight } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
import type { DType } from "@/client"
|
||||
import { Marquee } from "@/components/Common/Marquee"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { cn, si } from "@/lib/utils"
|
||||
|
||||
/** How much of a structured value is worth unfolding in a side panel. */
|
||||
const MAX_HEIGHT = "max-h-48"
|
||||
|
||||
function count(n: number, one: string, many = `${one}s`): string {
|
||||
return `${n} ${n === 1 ? one : many}`
|
||||
}
|
||||
|
||||
/** Whether this is an artifact reference rather than data of its own. */
|
||||
function isArtifact(value: Record<string, unknown>): boolean {
|
||||
return typeof value.digest === "string" && value.digest.startsWith("sha256:")
|
||||
}
|
||||
|
||||
/**
|
||||
* What a structured value *is*, in the space a value would have taken.
|
||||
*
|
||||
* The shape is what you want while wiring — that this port carries a record of
|
||||
* three fields, or a checkpoint of sixty kilobytes — and the contents are what
|
||||
* you want once something looks wrong. Serialising the whole thing into a line
|
||||
* answers the second question badly and the first one not at all.
|
||||
*/
|
||||
function summarize(value: object, dtype?: DType): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `${dtype ?? "list"} · ${count(value.length, "item")}`
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>
|
||||
if (isArtifact(record)) {
|
||||
const name =
|
||||
typeof record.name === "string" && record.name ? record.name : ""
|
||||
const size = typeof record.size === "number" ? `${si(record.size)}B` : ""
|
||||
return ["artifact", name, size].filter(Boolean).join(" · ")
|
||||
}
|
||||
|
||||
if (Array.isArray(record.lines)) {
|
||||
const points = record.lines.reduce(
|
||||
(total: number, line: unknown) =>
|
||||
total +
|
||||
(Array.isArray((line as { points?: unknown[] })?.points)
|
||||
? ((line as { points: unknown[] }).points.length as number)
|
||||
: 0),
|
||||
0,
|
||||
)
|
||||
return `series · ${count(record.lines.length, "line")}, ${count(points, "point")}`
|
||||
}
|
||||
|
||||
return `${dtype ?? "record"} · ${count(Object.keys(record).length, "field")}`
|
||||
}
|
||||
|
||||
/**
|
||||
* What a message is carrying: its shape at rest, its contents on request.
|
||||
*
|
||||
* A scalar simply reads, scrolling itself when it is longer than the room it
|
||||
* was given. Anything structured shows what it is and unfolds when asked —
|
||||
* which is what keeps a panel four hundred pixels wide from being pushed open
|
||||
* by one checkpoint reference.
|
||||
*/
|
||||
export function ValuePreview({
|
||||
value,
|
||||
dtype,
|
||||
defaultOpen = false,
|
||||
className,
|
||||
}: {
|
||||
value: unknown
|
||||
dtype?: DType
|
||||
/** Start unfolded, where there is room for it — an inspector, not a row. */
|
||||
defaultOpen?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
const structured = value !== null && typeof value === "object"
|
||||
|
||||
if (!structured) {
|
||||
return (
|
||||
<Marquee
|
||||
text={value === undefined ? "" : String(value)}
|
||||
className={cn("block font-mono text-xs", className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("min-w-0", className)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((was) => !was)}
|
||||
aria-expanded={open}
|
||||
className="flex w-full min-w-0 items-center gap-1 text-left text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||
data-testid="value-preview-toggle"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"size-3 shrink-0 transition-transform duration-[--duration-fast]",
|
||||
open && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
<Marquee text={summarize(value, dtype)} className="flex-1 font-mono" />
|
||||
</button>
|
||||
{open ? (
|
||||
<ScrollArea className={cn("mt-1", MAX_HEIGHT)}>
|
||||
<pre className="whitespace-pre-wrap break-all font-mono text-xs">
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
</ScrollArea>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user