diff --git a/website/src/components/CopyForLLM.tsx b/website/src/components/CopyForLLM.tsx new file mode 100644 index 000000000000..6234d6eb13b9 --- /dev/null +++ b/website/src/components/CopyForLLM.tsx @@ -0,0 +1,175 @@ +import { useEffect, useRef, useState } from 'react' + +interface CopyForLLMProps { + /** URL to the raw Markdown source of this page */ + rawUrl: string +} + +/** + * A "Copy page" button with a dropdown that lets users: + * 1. Copy the page's raw Markdown to the clipboard (great for LLMs) + * 2. Open the raw Markdown in a new tab + */ +export function CopyForLLM({ rawUrl }: CopyForLLMProps) { + const [open, setOpen] = useState(false) + const [status, setStatus] = useState<'idle' | 'copying' | 'copied'>('idle') + const menuRef = useRef(null) + + // Close the dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + const copyMarkdown = async () => { + setOpen(false) + setStatus('copying') + try { + const res = await fetch(rawUrl) + const text = await res.text() + await navigator.clipboard.writeText(text) + setStatus('copied') + setTimeout(() => setStatus('idle'), 2000) + } catch { + setStatus('idle') + } + } + + const label = status === 'copied' ? 'Copied!' : status === 'copying' ? 'Copying…' : 'Copy page' + + return ( +
+ {/* Primary button */} + + + {/* Chevron toggle */} + + + {/* Dropdown */} + {open && ( +
+ + + setOpen(false)} + className="flex items-start gap-3 px-4 py-3 transition hover:bg-space-1600" + > + +
+
View as Markdown ↗
+
View this page as plain text
+
+
+
+ )} +
+ ) +} + +// --------------------------------------------------------------------------- +// Inline SVG icons (avoids adding an icon library dependency) +// --------------------------------------------------------------------------- + +function ClipboardIcon({ className, size = 14 }: { className?: string; size?: number }) { + return ( + + ) +} + +function ChevronIcon({ open }: { open: boolean }) { + return ( + + ) +} + +function MarkdownIcon({ className }: { className?: string }) { + return ( + + ) +} diff --git a/website/src/components/index.ts b/website/src/components/index.ts index 6b48b4900732..35e2632293ff 100644 --- a/website/src/components/index.ts +++ b/website/src/components/index.ts @@ -1,6 +1,7 @@ export * from './Callout' export * from './Card' export * from './CodeBlock' +export * from './CopyForLLM' export * from './DocSearch' export * from './Heading' export * from './Image' diff --git a/website/src/layout/templates/default/content.tsx b/website/src/layout/templates/default/content.tsx index d8397b04f9ee..4bb6bf2a1dd6 100644 --- a/website/src/layout/templates/default/content.tsx +++ b/website/src/layout/templates/default/content.tsx @@ -3,6 +3,7 @@ import { type ComponentProps, useContext } from 'react' import { ButtonOrLink, classNames, ExperimentalDivider, ExperimentalLink } from '@edgeandnode/gds' import { ArrowLeft, ArrowRight, CalendarDynamic, HourglassDynamic, SocialGitHub } from '@edgeandnode/gds/icons' +import { CopyForLLM } from '@/components' import { useI18n } from '@/i18n' import { LayoutContext } from '../../shared' @@ -22,6 +23,17 @@ export default function TemplateDefaultContent({ className, children, ...props } return `https://github.com/graphprotocol/docs/blob/main/website/src/pages/${segments.map(encodeURIComponent).join('/')}` })() + // Derive the raw Markdown URL so users can copy it directly into an LLM. + // For remote pages (e.g. sourced from another repo), transform the GitHub + // blob URL into a raw.githubusercontent.com URL. For local pages, build + // the raw URL from the file path. + const rawMarkdownUrl = remotePageUrl + ? remotePageUrl.replace('https://github.com/', 'https://raw.githubusercontent.com/').replace('/blob/', '/') + : (() => { + const [_src, _pages, ...segments] = filePath.split('/') + return `https://raw.githubusercontent.com/graphprotocol/docs/main/website/src/pages/${segments.join('/')}` + })() + return (
) : null} - } - className="ms-auto text-space-700" - > - {t('global.page.edit')} - +
+ + } + className="text-space-700" + > + {t('global.page.edit')} + +