CopyPromptAdmonition.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { type RefObject } from 'react'
  2. import { Admonition } from 'ui-patterns/admonition'
  3. import CopyButton from '@/components/ui/CopyButton'
  4. import { BASE_PATH } from '@/lib/constants'
  5. interface CopyPromptAdmonitionProps {
  6. stepsContainerRef: RefObject<HTMLDivElement | null>
  7. }
  8. export function CopyPromptAdmonition({ stepsContainerRef }: CopyPromptAdmonitionProps) {
  9. const normalizeTextLines = (value: string) => {
  10. return value
  11. .split('\n')
  12. .map((line) => line.trim())
  13. .filter(Boolean)
  14. .join('\n')
  15. }
  16. const getStepTextContent = (contentElement: HTMLElement) => {
  17. const clone = contentElement.cloneNode(true) as HTMLElement
  18. clone
  19. .querySelectorAll('pre, button, svg, input, textarea, select, [aria-hidden="true"]')
  20. .forEach((element) => {
  21. element.remove()
  22. })
  23. clone.querySelectorAll('p, div').forEach((el) => {
  24. el.appendChild(document.createTextNode('\n'))
  25. })
  26. const text = clone.textContent ?? ''
  27. return normalizeTextLines(text)
  28. }
  29. const getStepCodeSnippets = (contentElement: HTMLElement) => {
  30. const snippets: Array<{ label: string; snippet: string }> = []
  31. const seen = new Set<string>()
  32. const addSnippet = (label: string, snippet: string) => {
  33. if (!snippet || seen.has(snippet)) return
  34. seen.add(snippet)
  35. snippets.push({ label, snippet })
  36. }
  37. const tabContents = Array.from(
  38. contentElement.querySelectorAll('[data-connect-tab-content]')
  39. ) as HTMLElement[]
  40. tabContents.forEach((tabContent) => {
  41. const label = tabContent.getAttribute('data-tab-label') || 'Code'
  42. const tabSnippets = Array.from(tabContent.querySelectorAll('pre'))
  43. .map((pre) => pre.textContent?.trim())
  44. .filter((snippet): snippet is string => Boolean(snippet))
  45. if (tabSnippets.length === 0) {
  46. const inlineSnippets = Array.from(tabContent.querySelectorAll('code'))
  47. .filter((code) => !code.closest('pre') && code.closest('.font-mono'))
  48. .map((code) => code.textContent?.trim())
  49. .filter((snippet): snippet is string => Boolean(snippet))
  50. inlineSnippets.forEach((snippet, index) => {
  51. const inlineLabel = inlineSnippets.length > 1 ? `${label} (part ${index + 1})` : label
  52. addSnippet(inlineLabel, snippet)
  53. })
  54. return
  55. }
  56. tabSnippets.forEach((snippet, index) => {
  57. const tabLabel = tabSnippets.length > 1 ? `${label} (part ${index + 1})` : label
  58. addSnippet(tabLabel, snippet)
  59. })
  60. })
  61. contentElement.querySelectorAll('pre').forEach((pre) => {
  62. if (pre.closest('[data-connect-tab-content]')) return
  63. const snippet = pre.textContent?.trim()
  64. if (snippet) addSnippet('Code', snippet)
  65. })
  66. contentElement.querySelectorAll('code').forEach((code) => {
  67. if (code.closest('pre')) return
  68. if (code.closest('[data-connect-tab-content]')) return
  69. if (!code.closest('.font-mono')) return
  70. const snippet = code.textContent?.trim()
  71. if (snippet) addSnippet('Code', snippet)
  72. })
  73. return snippets
  74. }
  75. const handleCopyPrompt = () => {
  76. const stepElements = stepsContainerRef.current?.querySelectorAll('[data-connect-step]')
  77. if (!stepElements?.length) return ''
  78. const promptContent = Array.from(stepElements)
  79. .map((stepElement, index) => {
  80. const title = stepElement.getAttribute('data-step-title') ?? `Step ${index + 1}`
  81. const description = stepElement.getAttribute('data-step-description') ?? ''
  82. const contentElement = stepElement.querySelector(
  83. '[data-step-content]'
  84. ) as HTMLElement | null
  85. const details = contentElement ? getStepTextContent(contentElement) : ''
  86. const codeSnippets = contentElement ? getStepCodeSnippets(contentElement) : []
  87. const sections = [
  88. `${index + 1}. ${title}`,
  89. description,
  90. details ? `Details:\n${details}` : null,
  91. codeSnippets.length
  92. ? `Code:\n${codeSnippets
  93. .map(({ label, snippet }) => `File: ${label}\n\`\`\`\n${snippet}\n\`\`\``)
  94. .join('\n\n')}`
  95. : null,
  96. ].filter(Boolean)
  97. return sections.join('\n')
  98. })
  99. .join('\n\n')
  100. return promptContent
  101. }
  102. return (
  103. <Admonition
  104. type="tip"
  105. showIcon={false}
  106. layout="horizontal"
  107. actions={<CopyButton type="default" copyLabel="Copy prompt" asyncText={handleCopyPrompt} />}
  108. >
  109. <div className="absolute -inset-16 z-0 opacity-50">
  110. <img
  111. src={`${BASE_PATH}/img/reports/bg-grafana-dark.svg`}
  112. alt="Briven Grafana"
  113. className="w-full h-full object-cover object-right hidden dark:block"
  114. />
  115. <img
  116. src={`${BASE_PATH}/img/reports/bg-grafana-light.svg`}
  117. alt="Briven Grafana"
  118. className="w-full h-full object-cover object-right dark:hidden"
  119. />
  120. <div className="absolute inset-0 bg-linear-to-r from-background-alternative to-transparent" />
  121. </div>
  122. <div className="relative flex flex-col md:flex-row md:items-center gap-y-2 md:gap-x-8 justify-between">
  123. <div className="flex flex-col gap-y-0.5">
  124. <p className="heading-default">Give your agent everything it needs</p>
  125. </div>
  126. </div>
  127. </Admonition>
  128. )
  129. }