'use client' import { useSendTelemetryEvent } from '~/lib/telemetry' import { type DocsAiPromptSource } from 'common/telemetry-constants' import { ChevronDown } from 'lucide-react' import { Children, isValidElement, useId, useState, type ReactElement, type ReactNode } from 'react' import ReactMarkdown from 'react-markdown' // shadcn tabs from packages/ui/src/components/shadcn/ui/tabs.tsx import { cn, Tabs, TabsContent, TabsIndicator, TabsList, TabsTrigger } from 'ui' import { CodeCopyButton } from './CodeBlock/CodeBlock.client' type PromptTitleProps = { children: ReactNode } type PromptCopyProps = { children: string } type PromptContentProps = { children: ReactNode /** Draws attention with a text shimmer. Defaults to true. */ shimmer?: boolean } type PromptProps = { children: ReactNode /** Stable tab id. Auto-generated when omitted. */ value?: string /** When true, content starts collapsed with a show more / show less control. */ expandable?: boolean } type PromptPanelTelemetry = { /** Surface the panel renders on. */ source: DocsAiPromptSource /** Prompt identifier, when the panel wraps a known prompt. */ promptId?: string } type PromptPanelProps = { children: ReactNode className?: string /** * Sends `docs_ai_prompt_copied` on a successful copy. Omit to leave the panel * untracked. */ telemetry?: PromptPanelTelemetry } type CollectedPrompt = { value: string title: ReactNode copyValue: string content: ReactNode expandable: boolean shimmer: boolean } function PromptTitle(_props: PromptTitleProps) { return null } PromptTitle.displayName = 'PromptTitle' function PromptCopy(_props: PromptCopyProps) { return null } PromptCopy.displayName = 'PromptCopy' function PromptContent(_props: PromptContentProps) { return null } PromptContent.displayName = 'PromptContent' function Prompt(_props: PromptProps) { return null } Prompt.displayName = 'Prompt' function isElementOfType(child: ReactNode, type: { displayName?: string }) { return ( isValidElement(child) && typeof child.type !== 'string' && 'displayName' in child.type && child.type.displayName === type.displayName ) } function collectPrompt(prompt: ReactElement, index: number): CollectedPrompt { let title: ReactNode = `Prompt ${index + 1}` let copyValue = '' let content: ReactNode = null let shimmer = true Children.forEach(prompt.props.children, (child) => { if (isElementOfType(child, PromptTitle)) { title = (child as ReactElement).props.children } else if (isElementOfType(child, PromptCopy)) { copyValue = (child as ReactElement).props.children } else if (isElementOfType(child, PromptContent)) { const props = (child as ReactElement).props content = props.children shimmer = props.shimmer !== false } }) return { value: prompt.props.value ?? `prompt-${index}`, title, copyValue, content, expandable: Boolean(prompt.props.expandable), shimmer, } } function collectPrompts(children: ReactNode): CollectedPrompt[] { return Children.toArray(children) .filter((child): child is ReactElement => isElementOfType(child, Prompt)) .map((prompt, index) => collectPrompt(prompt, index)) } function ExpandableContent({ children }: { children: ReactNode }) { const [isExpanded, setIsExpanded] = useState(false) const [isHoverRevealPaused, setIsHoverRevealPaused] = useState(false) const handleReelPointerLeave = () => setIsHoverRevealPaused(false) const handleToggle = () => { if (isExpanded) setIsHoverRevealPaused(true) setIsExpanded((expanded) => !expanded) } return (
{children}
) } function PromptBody({ prompt, shimmerEnabled, }: { prompt: CollectedPrompt shimmerEnabled: boolean }) { const [shimmerMounted, setShimmerMounted] = useState(prompt.shimmer) const content = (
{ if (event.target !== event.currentTarget) return if (shimmerMounted && !shimmerEnabled) { setShimmerMounted(false) } }} > {prompt.content}
) return (
{prompt.expandable ? {content} : content}
) } /** Renders a Markdown prompt string as the panel body, with backticks as code chips. */ function PromptMarkdown({ children }: { children: string }) { return (
{children}
) } /** Inline code chip for prompt bodies; opts out of the text shimmer. */ function PromptCode({ children }: { children?: ReactNode }) { return ( {children} ) } const MARKDOWN_COMPONENTS = { code: PromptCode } const tabTriggerClassName = cn( 'h-full px-0 py-0 text-xs shadow-none data-[state=active]:shadow-none', 'group-has-[[data-tab-indicator]]/list:border-b', 'group-data-[tab-indicator-ready]/list:data-[state=active]:border-b-transparent' ) /** * Copyable prompt card. Compose one or more `` children; multiple * prompts render as tabs. A single prompt still shows the header + copy * button, with the title styled like an inactive tab (no underline). * * @example * ```tsx * * * Agent Prompt * Plain text copied to the clipboard * Rich content shown in the panel * * * ``` */ function PromptPanel({ children, className, telemetry }: PromptPanelProps) { const fallbackId = useId() const titleId = useId() const prompts = collectPrompts(children) const [activeTab, setActiveTab] = useState(prompts[0]?.value ?? fallbackId) const [shimmerEnabled, setShimmerEnabled] = useState(true) const sendTelemetryEvent = useSendTelemetryEvent() if (prompts.length === 0) return null const activePrompt = prompts.find((prompt) => prompt.value === activeTab) ?? prompts[0] const hasTabs = prompts.length > 1 const dismissShimmer = () => setShimmerEnabled(false) const copyTarget = typeof activePrompt.title === 'string' ? activePrompt.title : 'content' const handleCopied = telemetry ? () => { sendTelemetryEvent({ action: 'docs_ai_prompt_copied', properties: { source: telemetry.source, tab: activePrompt.value, promptId: telemetry.promptId, }, }) } : undefined const header = (
{hasTabs ? ( {prompts.map((prompt) => ( {prompt.title} ))} ) : ( {prompts[0].title} )}
) if (!hasTabs) { return (
{header}
) } return ( {header} {/* Stack panes in one grid cell so the panel keeps the tallest tab's height. */}
{prompts.map((prompt) => { const isActive = prompt.value === activeTab return ( ) })}
) } export { Prompt, PromptCode, PromptContent, PromptCopy, PromptMarkdown, PromptPanel, PromptTitle } export type { PromptContentProps, PromptCopyProps, PromptPanelProps, PromptPanelTelemetry, PromptProps, PromptTitleProps, }