diff --git a/apify-docs-theme/src/theme/DocItemContent/index.js b/apify-docs-theme/src/theme/DocItemContent/index.js index 374e51ebe0..21d5b2945f 100644 --- a/apify-docs-theme/src/theme/DocItemContent/index.js +++ b/apify-docs-theme/src/theme/DocItemContent/index.js @@ -7,6 +7,8 @@ import MDXContent from '@theme/MDXContent'; import clsx from 'clsx'; import React from 'react'; +import styles from './styles.module.css'; + function useSyntheticTitle() { const { metadata, frontMatter, contentTitle } = useDoc(); const shouldRender = !frontMatter.hide_title && typeof contentTitle === 'undefined'; @@ -62,11 +64,15 @@ export default function DocItemContent({ children }) { const shouldShowLLMButtons = allowedPaths.some((path) => location.pathname.startsWith(path)) && !disallowedPaths.some((path) => location.pathname.includes(path)); - return ( -
- {syntheticTitle && {syntheticTitle}} - {shouldShowLLMButtons && } - {children} -
- ); + return ( +
+ {(syntheticTitle || shouldShowLLMButtons) && ( +
+ {syntheticTitle && {syntheticTitle}} + {shouldShowLLMButtons && } +
+ )} + {children} +
+ ); } diff --git a/apify-docs-theme/src/theme/DocItemContent/styles.module.css b/apify-docs-theme/src/theme/DocItemContent/styles.module.css new file mode 100644 index 0000000000..07b8c9a105 --- /dev/null +++ b/apify-docs-theme/src/theme/DocItemContent/styles.module.css @@ -0,0 +1,17 @@ +.docItemContent { + display: flex; + align-items: center; + flex-wrap: wrap; + /* move the h1 margin to padding */ + h1 { + margin-bottom: 0 !important; + } + padding-bottom: calc( + var(--ifm-h1-vertical-rhythm-bottom) * var(--ifm-leading) + ); + + @media (max-width: 767px) { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/apify-docs-theme/src/theme/LLMButtons/CopyForLLM/index.jsx b/apify-docs-theme/src/theme/LLMButtons/CopyForLLM/index.jsx deleted file mode 100644 index de68adbbbd..0000000000 --- a/apify-docs-theme/src/theme/LLMButtons/CopyForLLM/index.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useState } from 'react'; - -import styles from '../styles.module.css'; - -// Custom component for button text -function ButtonText({ isLoading, isCopied }) { - if (isLoading) { - return 'Copying...'; - } - if (isCopied) { - return 'Copied!'; - } - return 'Copy for LLM'; -} - -export default function CopyForLLM() { - const [isLoading, setIsLoading] = useState(false); - const [isCopied, setIsCopied] = useState(false); - - const handleCopy = async () => { - if (window.analytics) { - window.analytics.track('Clicked', { - app: 'docs', - button_text: 'Copy for LLM', - element: 'llm-buttons.copyForLLM', - }); - } - - try { - setIsLoading(true); - - const currentUrl = window.location.href; - const markdownUrl = `${currentUrl}.md`; - - // Fetch the markdown content - const response = await fetch(markdownUrl); - - if (!response.ok) { - throw new Error(`Failed to fetch markdown: ${response.status}`); - } - - const markdownContent = await response.text(); - - // Copy to clipboard - await navigator.clipboard.writeText(markdownContent); - - // Show success feedback - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - } catch (error) { - console.error('Failed to copy markdown content:', error); - } finally { - setIsLoading(false); - } - }; - - return ( - - ); -} diff --git a/apify-docs-theme/src/theme/LLMButtons/ViewAsMarkdown/index.jsx b/apify-docs-theme/src/theme/LLMButtons/ViewAsMarkdown/index.jsx deleted file mode 100644 index fc47cb4e4b..0000000000 --- a/apify-docs-theme/src/theme/LLMButtons/ViewAsMarkdown/index.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -import styles from '../styles.module.css'; - -export default function ViewAsMarkdown() { - const handleClick = () => { - if (window.analytics) { - window.analytics.track('Clicked', { - app: 'docs', - button_text: 'View as Markdown', - element: 'llm-buttons.viewAsMarkdown', - }); - } - - try { - const currentUrl = window.location.href; - const markdownUrl = `${currentUrl}.md`; - window.open(markdownUrl, '_blank'); - } catch (error) { - console.error('Error opening markdown file:', error); - } - }; - - return ( - - ); -} diff --git a/apify-docs-theme/src/theme/LLMButtons/index.jsx b/apify-docs-theme/src/theme/LLMButtons/index.jsx index 6ab85fa376..4e0aa64398 100644 --- a/apify-docs-theme/src/theme/LLMButtons/index.jsx +++ b/apify-docs-theme/src/theme/LLMButtons/index.jsx @@ -1,15 +1,295 @@ -import React from 'react'; +import clsx from 'clsx'; +import React, { useCallback, useState } from 'react'; + +import { + AnthropicIcon, + ChatGptIcon, + CheckIcon, + ChevronDownIcon, + CopyIcon, + ExternalLinkIcon, + LoaderIcon, + MarkdownIcon, + PerplexityIcon, +} from '@apify/ui-icons'; +import { Menu, Text, theme } from '@apify/ui-library'; -import CopyForLLM from './CopyForLLM'; import styles from './styles.module.css'; -import ViewAsMarkdown from './ViewAsMarkdown'; -export default function LLMButtons() { - return ( -
- -
- +const DROPDOWN_OPTIONS = [ + { + label: 'Copy for LLM', + description: 'Copy page as Markdown for LLMs', + showExternalIcon: false, + Icon: CopyIcon, + value: 'copyForLLM', + }, + { + label: 'View as Markdown', + description: 'View this page as plain text', + showExternalIcon: true, + Icon: MarkdownIcon, + value: 'viewAsMarkdown', + }, + { + label: 'Open in ChatGPT', + description: 'Ask questions about this page', + showExternalIcon: true, + Icon: ChatGptIcon, + value: 'openInChatGPT', + }, + { + label: 'Open in Claude', + description: 'Ask questions about this page', + showExternalIcon: true, + Icon: AnthropicIcon, + value: 'openInClaude', + }, + { + label: 'Open in Perplexity', + description: 'Ask questions about this page', + showExternalIcon: true, + Icon: PerplexityIcon, + value: 'openInPerplexity', + }, +]; + +const getPrompt = (currentUrl) => `Read from ${currentUrl} so I can ask questions about it.`; +const getMarkdownUrl = (currentUrl) => `${currentUrl}.md`; + +const onOpenInChatGPTClick = () => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Open in ChatGPT', + element: 'llm-buttons.openInChatGPT', + }); + } + + const prompt = getPrompt(window.location.href); + + try { + window.open( + `https://chatgpt.com/?hints=search&q=${encodeURIComponent(prompt)}`, + '_blank', + ); + } catch (error) { + console.error('Error opening ChatGPT:', error); + } +}; + +const onOpenInClaudeClick = () => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Open in Claude', + element: 'llm-buttons.openInClaude', + }); + } + + const prompt = getPrompt(window.location.href); + + try { + window.open( + `https://claude.ai/new?q=${encodeURIComponent(prompt)}`, + '_blank', + ); + } catch (error) { + console.error('Error opening Claude:', error); + } +}; + +const onOpenInPerplexityClick = () => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Open in Perplexity', + element: 'llm-buttons.openInPerplexity', + }); + } + + const prompt = getPrompt(window.location.href); + + try { + window.open( + `https://www.perplexity.ai/search/new?q=${encodeURIComponent( + prompt, + )}`, + '_blank', + ); + } catch (error) { + console.error('Error opening Perplexity:', error); + } +}; + +const onCopyAsMarkdownClick = async ({ setCopyingStatus }) => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Copy for LLM', + element: 'llm-buttons.copyForLLM', + }); + } + + const markdownUrl = getMarkdownUrl(window.location.href); + + try { + setCopyingStatus('loading'); + + // Fetch the markdown content + const response = await fetch(markdownUrl); + + if (!response.ok) { + throw new Error(`Failed to fetch markdown: ${response.status}`); + } + + const markdownContent = await response.text(); + + // Copy to clipboard + await navigator.clipboard.writeText(markdownContent); + + // Show success feedback + setCopyingStatus('copied'); + } catch (error) { + console.error('Failed to copy markdown content:', error); + } finally { + setTimeout(() => setCopyingStatus('idle'), 2000); + } +}; + +const onViewAsMarkdownClick = () => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'View as Markdown', + element: 'llm-buttons.viewAsMarkdown', + }); + } + + const markdownUrl = getMarkdownUrl(window.location.href); + + try { + window.open(markdownUrl, '_blank'); + } catch (error) { + console.error('Error opening markdown file:', error); + } +}; + +function getButtonText({ status }) { + switch (status) { + case 'loading': + return 'Copying...'; + case 'copied': + return 'Copied'; + default: + return 'Copy for LLM'; + } +} + +const MenuBase = ({ + children, + ref, + copyingStatus, + setCopyingStatus, + chevronIconRef, + ...props +}) => ( +
+
+
onCopyAsMarkdownClick({ setCopyingStatus })} + > + {copyingStatus === 'loading' && } + {copyingStatus === 'copied' && } + {copyingStatus === 'idle' && } +
+ onCopyAsMarkdownClick({ setCopyingStatus })} + className={styles.llmButtonText} + > + {getButtonText({ status: copyingStatus })} + +
+ +
+
+
+); + +const Option = ({ Icon, label, description, showExternalIcon }) => ( +
+ +
+ {label} + + {description} +
+ {showExternalIcon && ( + + )} +
+); + +export default function LLMButtons({ isApiReferencePage = false }) { + const [copyingStatus, setCopyingStatus] = useState('idle'); + const chevronIconRef = React.useRef(null); + + const onMenuOptionClick = useCallback((value) => { + switch (value) { + case 'copyForLLM': + onCopyAsMarkdownClick({ setCopyingStatus }); + break; + case 'viewAsMarkdown': + onViewAsMarkdownClick(); + break; + case 'openInChatGPT': + onOpenInChatGPTClick(); + break; + case 'openInClaude': + onOpenInClaudeClick(); + break; + case 'openInPerplexity': + onOpenInPerplexityClick(); + break; + default: + break; + } + }, []); + + return ( + chevronIconRef.current?.classList.toggle( + styles.chevronIconOpen, + isOpen, + ) + } + components={{ + MenuBase: (props) => ( + + ), + }} + onSelect={onMenuOptionClick} + options={DROPDOWN_OPTIONS} + renderOption={Option} + /> ); } diff --git a/apify-docs-theme/src/theme/LLMButtons/styles.module.css b/apify-docs-theme/src/theme/LLMButtons/styles.module.css index ecdf5efcb7..44ead9e774 100644 --- a/apify-docs-theme/src/theme/LLMButtons/styles.module.css +++ b/apify-docs-theme/src/theme/LLMButtons/styles.module.css @@ -1,57 +1,115 @@ -.llmButtonsContainer { +:global .menu-list[data-floating-ui-focusable] { + gap: 0.4rem; + padding: 0.6rem; +} + +.llmMenu { + margin-top: 1.2rem; + width: fit-content; + + @media (min-width: 768px) { + margin-left: auto; + } +} + +.llmButtonWrapper { display: flex; - align-items: center; - gap: 12px; - margin-top: -8px; - margin-bottom: calc(var(--ifm-h1-vertical-rhythm-bottom) * var(--ifm-leading)); + justify-content: flex-end; + + @media (min-width: 768px) { + /* hack to make the dropdown menu align to the right */ + width: 23.3rem; + } } -.llmButtonsSeparator { - width: 1px; - height: 16px; - background-color: var(--ifm-hr-background-color); +.llmMenuApiReferencePage { + margin-top: -0.8rem; + margin-bottom: calc(var(--ifm-h1-vertical-rhythm-bottom) * var(--ifm-leading)); + margin-left: 0 !important; + .llmButtonWrapper { + width: fit-content !important; + } } .llmButton { + width: fit-content; + display: inline-flex; + align-items: center; + height: 3rem; + display: flex; align-items: center; - background-color: transparent; - border: none; - height: 16px; + border-radius: 8px; + border: 1px solid var(--color-Neutral_SeparatorSubtle); + background-color: var(--color-Neutral_BackgroundMuted); + cursor: pointer; - padding: 0; - gap: 4px; + transition: background-color 0.2s ease-in-out; + + &:hover { + background-color: var(--color-Neutral_BackgroundMuted); + } } -.llmButtonIcon { - width: 16px; - height: 16px; - margin: 0 !important; - cursor: pointer; - background-size: contain; - background-repeat: no-repeat; - background-position: center; - display: inline-block; +.copyUpIconWrapper { + display: flex; + align-items: center; + justify-content: center; + padding-left: 0.8rem; + padding-right: 0.4rem; + height: 100%; +} + +.llmButtonText { + height: 100%; + display: flex; + align-items: center; + padding-right: 0.8rem; + min-width: 9.3rem; + + /* prevents font size glitch when loading the page */ + margin: 0px; + font-size: 1.4rem; + font-weight: 400; + font-family: Inter, sans-serif; } -.llmButtonIconBackgroundMarkdown { - background-image: url('https://docs.apify.com/img/markdown.svg'); +.chevronIconWrapper { + border-left: 1px solid var(--color-Neutral_SeparatorSubtle); + display: flex; + align-items: center; + justify-content: center; + padding-inline: 0.8rem; + height: 100%; +} +.chevronIcon { + transition: transform 0.2s ease-in-out; } -.llmButtonIconBackgroundCopy { - background-image: url('https://docs.apify.com/img/copy.svg'); +.chevronIconOpen { + transform: rotate(180deg); } -/* Dark theme adjustments */ -[data-theme='dark'] .llmButtonIcon { - color: #e0e0e0; +.menuOption { + padding-block: 0.4rem; + display: flex; + gap: 0.4rem; } -[data-theme='dark'] .llmButtonIconBackgroundMarkdown { - background-image: url('https://docs.apify.com/img/markdown-dark-theme.svg'); +.menuOptionIcon { + flex-shrink: 0; + margin-top: 0.4rem; +} + +.menuOptionText { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.2rem; } -[data-theme='dark'] .llmButtonIconBackgroundCopy { - background-image: url('https://docs.apify.com/img/copy-dark-theme.svg'); +.menuOptionExternalIcon { + flex-shrink: 0; + margin-top: 0.2rem; } diff --git a/docusaurus.config.js b/docusaurus.config.js index 3629fec129..323140d10b 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -200,7 +200,7 @@ module.exports = { // Find the first Heading h1 and add LLMButtons after it // eslint-disable-next-line max-len const headingRegex = /(]*as=\{"h1"\}[^>]*className=\{"openapi__heading"\}[^>]*children=\{[^}]*\}[^>]*>\s*<\/Heading>)/; - md = md.replace(headingRegex, '$1\n\n\n'); + md = md.replace(headingRegex, '$1\n\n\n'); return md; }, @@ -210,7 +210,7 @@ module.exports = { // Find the first Heading h1 and add LLMButtons after it // eslint-disable-next-line max-len const headingRegex = /(]*as=\{"h1"\}[^>]*className=\{"openapi__heading"\}[^>]*children=\{[^}]*\}[^>]*>\s*<\/Heading>)/; - md = md.replace(headingRegex, '$1\n\n\n'); + md = md.replace(headingRegex, '$1\n\n\n'); return md; }, diff --git a/package-lock.json b/package-lock.json index 5f2873b342..fc9fee531c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "apify-docs-theme" ], "dependencies": { - "@apify/ui-library": "^1.0.0", + "@apify/ui-icons": "^1.19.0", + "@apify/ui-library": "^1.97.2", "@docusaurus/core": "^3.8.1", "@docusaurus/faster": "^3.8.1", "@docusaurus/plugin-client-redirects": "^3.8.1", @@ -693,10 +694,11 @@ "dev": true }, "node_modules/@apify/ui-icons": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/@apify/ui-icons/-/ui-icons-1.18.2.tgz", - "integrity": "sha512-JrCzf5Xj2nLFQMFsau3aEjJx4VohGEIy2HaH+DM2tCWBVqGLKyuxfrgge4/3hdayyy4bZYFZARSF4e45Tz/CxQ==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@apify/ui-icons/-/ui-icons-1.19.0.tgz", + "integrity": "sha512-oKzVVnGdRH+A3lWLUs0W7p5e0l/1Exu8XR/po+XtV7XAHkEWPjp6Qgz4oD3yUzs7/3jcYBVHsXDosHBkc5U8rw==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "clsx": "^2.0.0" }, @@ -706,11 +708,12 @@ } }, "node_modules/@apify/ui-library": { - "version": "1.97.0", - "resolved": "https://registry.npmjs.org/@apify/ui-library/-/ui-library-1.97.0.tgz", - "integrity": "sha512-2+fCbLZ9Jj/BTltu0JMWhRC7jhOL50Fc7kzHbxMN84fZX9vT6wbmoPog3WLfUWnz5XHUwhbxoLIf+x6PBQJI6w==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/@apify/ui-library/-/ui-library-1.98.0.tgz", + "integrity": "sha512-cxJWDA4vNuijS5/8F6z0pMJQ0X8NPkCBp5hNQnwHSSbzDlqI4aqdFcl40H2SHkS9A/GSm80Pfs8ATGFMFahndg==", + "license": "Apache-2.0", "dependencies": { - "@apify/ui-icons": "^1.18.2", + "@apify/ui-icons": "^1.18.3", "@floating-ui/react": "^0.26.2", "@react-hook/resize-observer": "^2.0.2", "clsx": "^2.0.0", diff --git a/package.json b/package.json index a3fb0dcb69..35d15535f8 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "typescript-eslint": "^8.38.0" }, "dependencies": { - "@apify/ui-library": "^1.0.0", + "@apify/ui-library": "^1.97.2", + "@apify/ui-icons": "^1.19.0", "@docusaurus/core": "^3.8.1", "@docusaurus/faster": "^3.8.1", "@docusaurus/plugin-client-redirects": "^3.8.1",