From 6dbf00915a1aaf2d1b42790ae3bd7d91fc78f47b Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Mon, 14 Jul 2025 11:43:11 -0700 Subject: [PATCH 1/4] feat(ai): Add copy page button to all doc pages --- src/components/copyMarkdownButton.tsx | 166 ++++++++++++++++++++++++++ src/components/docPage/index.tsx | 15 +-- src/hooks/usePlausibleEvent.tsx | 12 ++ 3 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 src/components/copyMarkdownButton.tsx diff --git a/src/components/copyMarkdownButton.tsx b/src/components/copyMarkdownButton.tsx new file mode 100644 index 00000000000000..6a8eee7d669069 --- /dev/null +++ b/src/components/copyMarkdownButton.tsx @@ -0,0 +1,166 @@ +'use client'; + +import {Fragment, useEffect, useRef, useState} from 'react'; +import {createPortal} from 'react-dom'; +import {Clipboard} from 'react-feather'; +import Link from 'next/link'; + +import {usePlausibleEvent} from 'sentry-docs/hooks/usePlausibleEvent'; +import Chevron from 'sentry-docs/icons/Chevron'; +import Markdown from 'sentry-docs/icons/Markdown'; + +interface CopyMarkdownButtonProps { + pathname: string; +} + +export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) { + const [isLoading, setIsLoading] = useState(false); + const [copied, setCopied] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [isMounted, setIsMounted] = useState(false); + const buttonRef = useRef(null); + const dropdownRef = useRef(null); + const {emit} = usePlausibleEvent(); + + const copyMarkdownToClipboard = async () => { + setIsLoading(true); + setCopied(false); + setIsOpen(false); + + emit('Copy Page', {props: {page: pathname, source: 'copy_button'}}); + + try { + const response = await fetch(`https://docs.sentry.io/${pathname}.md`); + if (!response.ok) { + throw new Error(`Failed to fetch markdown content: ${response.status}`); + } + + await navigator.clipboard.writeText(await response.text()); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error('Failed to copy markdown to clipboard:', error); + } finally { + setIsLoading(false); + } + }; + + const handleViewMarkdownClick = () => { + emit('View Markdown', {props: {page: pathname, source: 'view_link'}}); + setIsOpen(false); + }; + + const handleDropdownToggle = () => { + setIsOpen(!isOpen); + if (!isOpen) { + emit('Copy Page Dropdown', {props: {page: pathname, action: 'open'}}); + } + }; + + useEffect(() => { + setIsMounted(true); + + const handleClickOutside = (event: MouseEvent) => { + if ( + buttonRef.current && + !buttonRef.current.contains(event.target as Node) && + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const getDropdownPosition = () => { + if (!buttonRef.current) return {top: 0, left: 0}; + const rect = buttonRef.current.getBoundingClientRect(); + return { + top: rect.bottom + 8, + left: rect.right - 320, + }; + }; + + const buttonClass = "inline-flex items-center h-full text-gray-700 bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-50 active:bg-gray-100 focus:bg-gray-50 outline-none"; + const dropdownItemClass = "flex items-center gap-3 w-full p-2 px-3 text-left bg-transparent rounded-md transition-colors hover:bg-gray-100 font-sans text-gray-900"; + const iconContainerClass = "flex items-center justify-center w-7 h-7 bg-gray-100 rounded shrink-0"; + + return ( + +
+
+ + +
+ + +
+
+ + {isMounted && isOpen && createPortal( +
+
+ + + +
+ +
+
+
View as Markdown
+
View this page as plain text
+
+ +
+
, + document.body + )} + + ); +} diff --git a/src/components/docPage/index.tsx b/src/components/docPage/index.tsx index 03a72a6a7024f6..c2407de9da7737 100644 --- a/src/components/docPage/index.tsx +++ b/src/components/docPage/index.tsx @@ -1,8 +1,6 @@ import {ReactNode} from 'react'; -import Link from 'next/link'; import {getCurrentGuide, getCurrentPlatform, nodeForPath} from 'sentry-docs/docTree'; -import Markdown from 'sentry-docs/icons/Markdown'; import {serverContext} from 'sentry-docs/serverContext'; import {FrontMatter} from 'sentry-docs/types'; import {PaginationNavNode} from 'sentry-docs/types/paginationNavNode'; @@ -14,6 +12,7 @@ import './type.scss'; import {Banner} from '../banner'; import {Breadcrumbs} from '../breadcrumbs'; import {CodeContextProvider} from '../codeContext'; +import {CopyMarkdownButton} from '../copyMarkdownButton'; import {DocFeedback} from '../docFeedback'; import {GitHubCTA} from '../githubCTA'; import {Header} from '../header'; @@ -85,15 +84,9 @@ export function DocPage({
{leafNode && }{' '} - - - +
+ +
diff --git a/src/hooks/usePlausibleEvent.tsx b/src/hooks/usePlausibleEvent.tsx index 1976dbc6f2d1fb..308bc3a8a97f3b 100644 --- a/src/hooks/usePlausibleEvent.tsx +++ b/src/hooks/usePlausibleEvent.tsx @@ -8,6 +8,14 @@ type PlausibleEventProps = { page: string; title: string; }; + ['Copy Page']: { + page: string; + source: string; + }; + ['Copy Page Dropdown']: { + page: string; + action: string; + }; ['Doc Feedback']: { helpful: boolean; page: string; @@ -20,6 +28,10 @@ type PlausibleEventProps = { page: string; readProgress: ReadProgressMilestone; }; + ['View Markdown']: { + page: string; + source: string; + }; }; /** From 2f164ddfcebc797c482117cf3c2a7d7e5575dd4d Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Mon, 14 Jul 2025 12:38:06 -0700 Subject: [PATCH 2/4] fix url fetch gen logic to take multiple origins into account --- src/components/copyMarkdownButton.tsx | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/copyMarkdownButton.tsx b/src/components/copyMarkdownButton.tsx index 6a8eee7d669069..6b866d9dcf6a6c 100644 --- a/src/components/copyMarkdownButton.tsx +++ b/src/components/copyMarkdownButton.tsx @@ -16,6 +16,7 @@ interface CopyMarkdownButtonProps { export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) { const [isLoading, setIsLoading] = useState(false); const [copied, setCopied] = useState(false); + const [error, setError] = useState(false); const [isOpen, setIsOpen] = useState(false); const [isMounted, setIsMounted] = useState(false); const buttonRef = useRef(null); @@ -25,12 +26,15 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) { const copyMarkdownToClipboard = async () => { setIsLoading(true); setCopied(false); + setError(false); setIsOpen(false); emit('Copy Page', {props: {page: pathname, source: 'copy_button'}}); try { - const response = await fetch(`https://docs.sentry.io/${pathname}.md`); + // This doesn't work on local development since we need the generated markdown + // files, and we need to be aware of the origin since we have two different origins. + const response = await fetch(`${window.location.origin}/${pathname}.md`); if (!response.ok) { throw new Error(`Failed to fetch markdown content: ${response.status}`); } @@ -38,8 +42,10 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) { await navigator.clipboard.writeText(await response.text()); setCopied(true); setTimeout(() => setCopied(false), 2000); - } catch (error) { - console.error('Failed to copy markdown to clipboard:', error); + } catch (err) { + console.error('Failed to copy markdown to clipboard:', err); + setError(true); + setTimeout(() => setError(false), 3000); } finally { setIsLoading(false); } @@ -96,12 +102,12 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
@@ -137,8 +143,12 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
-
Copy page
-
Copy page as Markdown for LLMs
+
+ {error ? 'Failed to copy' : 'Copy page'} +
+
+ {error ? 'Network error - please try again' : 'Copy page as Markdown for LLMs'} +
From 532cd241e13309b0303255324f5bb716372f1bc7 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Mon, 14 Jul 2025 12:47:55 -0700 Subject: [PATCH 3/4] add dark mode support --- src/components/copyMarkdownButton.tsx | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/components/copyMarkdownButton.tsx b/src/components/copyMarkdownButton.tsx index 6b866d9dcf6a6c..73bb04783fd11a 100644 --- a/src/components/copyMarkdownButton.tsx +++ b/src/components/copyMarkdownButton.tsx @@ -43,7 +43,6 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) { setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { - console.error('Failed to copy markdown to clipboard:', err); setError(true); setTimeout(() => setError(false), 3000); } finally { @@ -92,17 +91,17 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) { }; }; - const buttonClass = "inline-flex items-center h-full text-gray-700 bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-50 active:bg-gray-100 focus:bg-gray-50 outline-none"; - const dropdownItemClass = "flex items-center gap-3 w-full p-2 px-3 text-left bg-transparent rounded-md transition-colors hover:bg-gray-100 font-sans text-gray-900"; - const iconContainerClass = "flex items-center justify-center w-7 h-7 bg-gray-100 rounded shrink-0"; + const buttonClass = "inline-flex items-center h-full text-gray-700 dark:text-[var(--foreground)] bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-50 dark:hover:bg-[var(--gray-a4)] active:bg-gray-100 dark:active:bg-[var(--gray-5)] focus:bg-gray-50 dark:focus:bg-[var(--gray-a4)] outline-none"; + const dropdownItemClass = "flex items-center gap-3 w-full p-2 px-3 text-left bg-transparent rounded-md transition-colors hover:bg-gray-100 dark:hover:bg-[var(--gray-a4)] font-sans text-gray-900 dark:text-[var(--foreground)]"; + const iconContainerClass = "flex items-center justify-center w-7 h-7 bg-gray-100 dark:bg-[var(--gray-a4)] rounded shrink-0"; return (
-
+
-
+
-
+
{error ? 'Failed to copy' : 'Copy page'}
-
+
{error ? 'Network error - please try again' : 'Copy page as Markdown for LLMs'}
@@ -163,8 +162,8 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
-
View as Markdown
-
View this page as plain text
+
View as Markdown
+
View this page as plain text
From 7d99a44a4b94047447158b8942c06a8b8c046115 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:49:12 +0000 Subject: [PATCH 4/4] [getsentry/action-github-commit] Auto commit --- src/components/copyMarkdownButton.tsx | 101 ++++++++++++++------------ src/hooks/usePlausibleEvent.tsx | 2 +- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/src/components/copyMarkdownButton.tsx b/src/components/copyMarkdownButton.tsx index 73bb04783fd11a..d3de2536cacbd2 100644 --- a/src/components/copyMarkdownButton.tsx +++ b/src/components/copyMarkdownButton.tsx @@ -91,9 +91,12 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) { }; }; - const buttonClass = "inline-flex items-center h-full text-gray-700 dark:text-[var(--foreground)] bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-50 dark:hover:bg-[var(--gray-a4)] active:bg-gray-100 dark:active:bg-[var(--gray-5)] focus:bg-gray-50 dark:focus:bg-[var(--gray-a4)] outline-none"; - const dropdownItemClass = "flex items-center gap-3 w-full p-2 px-3 text-left bg-transparent rounded-md transition-colors hover:bg-gray-100 dark:hover:bg-[var(--gray-a4)] font-sans text-gray-900 dark:text-[var(--foreground)]"; - const iconContainerClass = "flex items-center justify-center w-7 h-7 bg-gray-100 dark:bg-[var(--gray-a4)] rounded shrink-0"; + const buttonClass = + 'inline-flex items-center h-full text-gray-700 dark:text-[var(--foreground)] bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-50 dark:hover:bg-[var(--gray-a4)] active:bg-gray-100 dark:active:bg-[var(--gray-5)] focus:bg-gray-50 dark:focus:bg-[var(--gray-a4)] outline-none'; + const dropdownItemClass = + 'flex items-center gap-3 w-full p-2 px-3 text-left bg-transparent rounded-md transition-colors hover:bg-gray-100 dark:hover:bg-[var(--gray-a4)] font-sans text-gray-900 dark:text-[var(--foreground)]'; + const iconContainerClass = + 'flex items-center justify-center w-7 h-7 bg-gray-100 dark:bg-[var(--gray-a4)] rounded shrink-0'; return ( @@ -126,50 +129,58 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
- {isMounted && isOpen && createPortal( -
-
- + + +
+
-
- {error ? 'Network error - please try again' : 'Copy page as Markdown for LLMs'} +
+
+ View as Markdown +
+
+ View this page as plain text +
-
- - - -
- -
-
-
View as Markdown
-
View this page as plain text
-
- -
-
, - document.body - )} + +
+
, + document.body + )}
); } diff --git a/src/hooks/usePlausibleEvent.tsx b/src/hooks/usePlausibleEvent.tsx index 308bc3a8a97f3b..d4a3d06341ff18 100644 --- a/src/hooks/usePlausibleEvent.tsx +++ b/src/hooks/usePlausibleEvent.tsx @@ -13,8 +13,8 @@ type PlausibleEventProps = { source: string; }; ['Copy Page Dropdown']: { - page: string; action: string; + page: string; }; ['Doc Feedback']: { helpful: boolean;