'use client' import { Check, Copy } from 'lucide-react' import { Children, isValidElement, useEffect, useId, useState, type ReactElement, type ReactNode, } from 'react' // shadcn tabs from packages/ui/src/components/shadcn/ui/tabs.tsx import { cn, copyToClipboard, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui' type PromptTitleProps = { children: ReactNode icon?: 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 PromptPanelProps = { children: ReactNode className?: string } type CollectedPrompt = { value: string title: ReactNode icon?: 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 icon: ReactNode | undefined let copyValue = '' let content: ReactNode = null let shimmer = true Children.forEach(prompt.props.children, (child) => { if (isElementOfType(child, PromptTitle)) { const props = (child as ReactElement).props title = props.children icon = props.icon } 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, icon, 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) return (
{children}
{!isExpanded && (
)}
) } 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}
) } function CopyButton({ label, value }: { label: string; value: string }) { const [copied, setCopied] = useState(false) useEffect(() => { if (!copied) return const timeout = window.setTimeout(() => setCopied(false), 2000) return () => window.clearTimeout(timeout) }, [copied]) return ( ) } function TabLabel({ icon, children }: { icon?: ReactNode; children: ReactNode }) { return ( {icon && ( )} {children} ) } const tabTriggerClassName = 'h-full px-0 py-0 text-xs shadow-none data-[state=active]:shadow-none' /** * 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 * * * }>AI Prompt * Plain text copied to the clipboard * Rich content shown in the panel * * * ``` */ function PromptPanel({ children, className }: PromptPanelProps) { const fallbackId = useId() const titleId = useId() const prompts = collectPrompts(children) const [activeTab, setActiveTab] = useState(prompts[0]?.value ?? fallbackId) const [shimmerEnabled, setShimmerEnabled] = useState(true) 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 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, PromptContent, PromptCopy, PromptPanel, PromptTitle } export type { PromptContentProps, PromptCopyProps, PromptPanelProps, PromptProps, PromptTitleProps }