import { CheckIcon, ClipboardDocumentIcon } from "@heroicons/react/20/solid"; import { Suspense, useState } from "react"; import { Button } from "~/components/primitives/Buttons"; import { StreamdownRenderer } from "~/components/code/StreamdownRenderer"; import { Header3 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { TabButton, TabContainer } from "~/components/primitives/Tabs"; import { TextLink } from "~/components/primitives/TextLink"; import { tryPrettyJson } from "./aiHelpers"; import { SpanMetricRow as PromptMetricRow } from "./SpanMetricRow"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useHasAdminAccess } from "~/hooks/useUser"; import { v3PromptPath } from "~/utils/pathBuilder"; import { CodeBlock } from "~/components/code/CodeBlock"; import { AIChatMessages, AssistantResponse, ChatBubble, type PromptLink } from "./AIChatMessages"; import { AIStatsSummary, AITagsRow } from "./AIModelSummary"; import { AIToolsInventory } from "./AIToolsInventory"; import type { AISpanData, DisplayItem } from "./types"; import type { PromptSpanData } from "~/presenters/v3/SpanPresenter.server"; import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline"; type AITab = "overview" | "messages" | "tools" | "prompt"; export function AISpanDetails({ aiData, promptVersionData, rawProperties, startTime, duration, }: { aiData: AISpanData; promptVersionData?: PromptSpanData; rawProperties?: string; startTime?: string | Date; duration?: number | null; }) { const [tab, setTab] = useState("overview"); const isAdmin = useHasAdminAccess(); const toolCount = aiData.toolCount ?? aiData.toolDefinitions?.length ?? 0; const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const promptLink: PromptLink | undefined = aiData.promptSlug && organization && project && environment ? { slug: aiData.promptSlug, version: aiData.promptVersion, path: v3PromptPath( organization, project, environment, aiData.promptSlug, aiData.promptVersion ), } : undefined; return (
{/* Tab bar */}
setTab("overview")} shortcut={{ key: "o" }} > Overview setTab("messages")} shortcut={{ key: "m" }} > Messages setTab("tools")} shortcut={{ key: "t" }} > Tools {toolCount > 0 && ( {toolCount} )} {promptVersionData && ( setTab("prompt")} shortcut={{ key: "p" }} > Prompt )}
{/* Tab content */}
{tab === "overview" && ( <> {startTime && (
)} )} {tab === "messages" && } {tab === "tools" && } {tab === "prompt" && promptVersionData && ( )}
{/* Footer: Copy raw (admin only) */} {isAdmin && rawProperties && }
); } function OverviewTab({ aiData }: { aiData: AISpanData }) { const { userText, outputText, outputObject, outputToolNames } = extractInputOutput(aiData); return (
{userText && (
Input {userText}
)} {outputText && } {!outputText && outputObject && (
Output
)} {outputToolNames.length > 0 && !outputText && !outputObject && (
Output Called {outputToolNames.length === 1 ? "tool" : "tools"}:{" "} {outputToolNames.join(", ")}
)}
); } function MessagesTab({ aiData, promptLink }: { aiData: AISpanData; promptLink?: PromptLink }) { const showFallbackText = aiData.responseText && !hasAssistantItem(aiData.items); const showFallbackObject = !showFallbackText && aiData.responseObject && !hasAssistantItem(aiData.items); return (
{aiData.items && aiData.items.length > 0 && ( )} {showFallbackText && } {showFallbackObject && (
Assistant
)}
); } function ToolsTab({ aiData }: { aiData: AISpanData }) { return ; } function CopyRawFooter({ rawProperties }: { rawProperties: string }) { const [copied, setCopied] = useState(false); function handleCopy() { navigator.clipboard.writeText(rawProperties); setCopied(true); setTimeout(() => setCopied(false), 2000); } return (
); } function PromptTab({ promptData, promptLink, }: { promptData: PromptSpanData; promptLink?: PromptLink; }) { const labels = promptData.labels ? promptData.labels .split(",") .map((l) => l.trim()) .filter(Boolean) : []; return (
{/* Prompt properties */}
{promptData.slug} ) : ( promptData.slug ) } /> {labels.length > 0 && } {promptData.model && }
{/* Prompt input (variables passed to resolve()) */} {promptData.input && (
Input
)} {/* Template (from the prompt version) */} {promptData.template && (
Template
{promptData.template}} > {promptData.template}
)}
); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function extractInputOutput(aiData: AISpanData): { userText: string | undefined; outputText: string | undefined; outputObject: string | undefined; outputToolNames: string[]; } { let userText: string | undefined; let outputText: string | undefined; const outputToolNames: string[] = []; if (aiData.items) { for (let i = aiData.items.length - 1; i >= 0; i--) { if (aiData.items[i].type === "user") { userText = (aiData.items[i] as { type: "user"; text: string }).text; break; } } for (let i = aiData.items.length - 1; i >= 0; i--) { const item = aiData.items[i]; if (item.type === "assistant") { outputText = item.text; break; } if (item.type === "tool-use") { for (const tool of item.tools) { outputToolNames.push(tool.toolName); } break; } } } if (!outputText && aiData.responseText) { outputText = aiData.responseText; } return { userText, outputText, outputObject: aiData.responseObject ? tryPrettyJson(aiData.responseObject) : undefined, outputToolNames, }; } function hasAssistantItem(items: DisplayItem[] | undefined): boolean { if (!items) return false; return items.some((item) => item.type === "assistant"); }