Files
2026-07-13 13:32:57 +08:00

149 lines
5.5 KiB
TypeScript

import { Suspense, useState } from "react";
import { CodeBlock } from "~/components/code/CodeBlock";
import { StreamdownRenderer } from "~/components/code/StreamdownRenderer";
import { Header3 } from "~/components/primitives/Headers";
import { TextLink } from "~/components/primitives/TextLink";
import { tryPrettyJson } from "./ai/aiHelpers";
import { SpanMetricRow as MetricRow } from "./ai/SpanMetricRow";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { v3PromptPath } from "~/utils/pathBuilder";
import { TabButton, TabContainer } from "~/components/primitives/Tabs";
import type { PromptSpanData } from "~/presenters/v3/SpanPresenter.server";
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
type PromptTab = "overview" | "input" | "template";
export function PromptSpanDetails({
promptData,
startTime,
duration,
}: {
promptData: PromptSpanData;
startTime?: string | Date;
duration?: number | null;
}) {
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const promptPath =
organization && project && environment
? v3PromptPath(organization, project, environment, promptData.slug, promptData.version)
: undefined;
const hasInput = !!promptData.input;
const hasTemplate = !!promptData.template;
const availableTabs: PromptTab[] = [
"overview",
...(hasInput ? (["input"] as const) : []),
...(hasTemplate ? (["template"] as const) : []),
];
const [tab, setTab] = useState<PromptTab>("overview");
const labels = promptData.labels
? promptData.labels
.split(",")
.map((l) => l.trim())
.filter(Boolean)
: [];
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="shrink-0 overflow-x-auto px-3 py-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
<TabContainer>
{availableTabs.map((t) => (
<TabButton
key={t}
isActive={tab === t}
layoutId="prompt-span"
onClick={() => setTab(t)}
shortcut={
t === "overview" ? { key: "o" } : t === "input" ? { key: "i" } : { key: "t" }
}
>
{t === "overview" ? "Overview" : t === "input" ? "Input" : "Template"}
</TabButton>
))}
</TabContainer>
</div>
<div className="scrollbar-gutter-stable min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
{tab === "overview" && (
<div className="flex flex-col px-3">
{startTime && (
<SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} />
)}
<div className="flex flex-col gap-1 py-2.5">
<div className="flex flex-col text-xs @container">
<MetricRow
label="Prompt"
value={
promptPath ? (
<TextLink to={promptPath}>{promptData.slug}</TextLink>
) : (
promptData.slug
)
}
/>
<MetricRow label="Version" value={`v${promptData.version}`} />
{labels.length > 0 && <MetricRow label="Labels" value={labels.join(", ")} />}
{promptData.model && <MetricRow label="Model" value={promptData.model} />}
</div>
</div>
{promptData.text && (
<div className="flex flex-col gap-1.5 py-2.5">
<Header3>Resolved content</Header3>
<div className="rounded-md border border-grid-bright bg-background-hover/50 px-3.5 py-2">
<div className="font-sans text-sm font-normal text-text-dimmed streamdown-container">
<Suspense
fallback={
<span className="whitespace-pre-wrap">
{promptData.text.length > 300
? promptData.text.slice(0, 300) + "..."
: promptData.text}
</span>
}
>
<StreamdownRenderer>{promptData.text}</StreamdownRenderer>
</Suspense>
</div>
</div>
</div>
)}
</div>
)}
{tab === "input" && hasInput && (
<div className="px-3 py-2.5">
<CodeBlock
code={tryPrettyJson(promptData.input!)}
maxLines={30}
showLineNumbers={false}
showCopyButton
language="json"
/>
</div>
)}
{tab === "template" && hasTemplate && (
<div className="px-3 py-2.5">
<div className="rounded-md border border-grid-bright bg-background-hover/50 px-3.5 py-2">
<div className="font-sans text-sm font-normal text-text-dimmed streamdown-container">
<Suspense
fallback={<span className="whitespace-pre-wrap">{promptData.template!}</span>}
>
<StreamdownRenderer>{promptData.template!}</StreamdownRenderer>
</Suspense>
</div>
</div>
</div>
)}
</div>
</div>
);
}