From 5455d615e4532ab137de52413b29f6831b813556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20W=C3=A9br?= Date: Tue, 30 Sep 2025 16:02:29 +0200 Subject: [PATCH 01/18] feat(docs): LLMs dropdown --- .../src/theme/DocItemContent/index.js | 17 +- .../theme/DocItemContent/styles.module.css | 8 + .../src/theme/LLMButtons/CopyForLLM/index.jsx | 71 -------- .../theme/LLMButtons/ViewAsMarkdown/index.jsx | 37 ----- .../src/theme/LLMButtons/index.jsx | 153 +++++++++++++++++- .../src/theme/LLMButtons/styles.module.css | 73 ++++----- package-lock.json | 19 ++- package.json | 3 +- 8 files changed, 211 insertions(+), 170 deletions(-) create mode 100644 apify-docs-theme/src/theme/DocItemContent/styles.module.css delete mode 100644 apify-docs-theme/src/theme/LLMButtons/CopyForLLM/index.jsx delete mode 100644 apify-docs-theme/src/theme/LLMButtons/ViewAsMarkdown/index.jsx diff --git a/apify-docs-theme/src/theme/DocItemContent/index.js b/apify-docs-theme/src/theme/DocItemContent/index.js index 374e51ebe0..8639b5ecf8 100644 --- a/apify-docs-theme/src/theme/DocItemContent/index.js +++ b/apify-docs-theme/src/theme/DocItemContent/index.js @@ -6,6 +6,7 @@ import LLMButtons from '@theme/LLMButtons'; 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(); @@ -62,11 +63,13 @@ 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 && {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..b54a07f28b --- /dev/null +++ b/apify-docs-theme/src/theme/DocItemContent/styles.module.css @@ -0,0 +1,8 @@ +.docItemContent { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + column-gap: 12px; + flex-wrap: wrap; +} 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..34c9e96806 100644 --- a/apify-docs-theme/src/theme/LLMButtons/index.jsx +++ b/apify-docs-theme/src/theme/LLMButtons/index.jsx @@ -1,15 +1,152 @@ -import React from 'react'; +import React, { useState } from 'react'; -import CopyForLLM from './CopyForLLM'; import styles from './styles.module.css'; -import ViewAsMarkdown from './ViewAsMarkdown'; +import { Menu, Text, theme, PlainMenuBaseComponent } from '@apify/ui-library'; +import { + CopyIcon, + ExternalLinkIcon, + MarkdownIcon, + ChevronDownIcon, +} from '@apify/ui-icons'; export default function LLMButtons() { + const [isCopyingLoading, setCopyingIsLoading] = useState(false); + const [isCopied, setIsCopied] = useState(false); + + const onCopyAsMarkdownClick = async () => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Copy for LLM', + element: 'llm-buttons.copyForLLM', + }); + } + + try { + setCopyingIsLoading(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 { + setCopyingIsLoading(false); + } + }; + + const onViewAsMarkdownClick = () => { + 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); + } + }; + + const onMenuOptionClick = (value) => { + switch (value) { + case 'copyForLLM': + onCopyAsMarkdownClick(); + break; + case 'viewAsMarkdown': + onViewAsMarkdownClick(); + break; + default: + break; + } + }; + return ( -
- -
- -
+ { + console.log(props); + return ( +
+ + + {isCopyingLoading + ? 'Copying...' + : isCopied + ? 'Copied!' + : 'Copy for LLM'} + + +
+ ); + }, + }} + onSelect={onMenuOptionClick} + options={[ + { + label: 'Copy page', + 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', + }, + ]} + renderOption={(option) => ( +
+ +
+ {option.label} + + {option.description} + +
+ {option.showExternalIcon && ( + + )} +
+ )} + /> ); } diff --git a/apify-docs-theme/src/theme/LLMButtons/styles.module.css b/apify-docs-theme/src/theme/LLMButtons/styles.module.css index ecdf5efcb7..741355cf6c 100644 --- a/apify-docs-theme/src/theme/LLMButtons/styles.module.css +++ b/apify-docs-theme/src/theme/LLMButtons/styles.module.css @@ -1,57 +1,54 @@ -.llmButtonsContainer { - display: flex; +.llmButton { + display: inline-flex; align-items: center; - gap: 12px; - margin-top: -8px; - margin-bottom: calc(var(--ifm-h1-vertical-rhythm-bottom) * var(--ifm-leading)); -} + gap: 4px; + height: 30px; -.llmButtonsSeparator { - width: 1px; - height: 16px; - background-color: var(--ifm-hr-background-color); -} + margin-block: 8px; -.llmButton { display: flex; align-items: center; - background-color: transparent; - border: none; - height: 16px; - cursor: pointer; - padding: 0; gap: 4px; -} + border-radius: 8px; + padding-inline: 8px; + border: 1px solid var(--color-Neutral_SeparatorSubtle); + 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; -} + transition: background-color 0.2s ease-in-out; -.llmButtonIconBackgroundMarkdown { - background-image: url('https://docs.apify.com/img/markdown.svg'); + &:hover { + background-color: var(--color-Neutral_BackgroundMuted); + } +} +.llmButtonText { + border-right: 1px solid var(--color-Neutral_SeparatorSubtle); + height: 100%; + display: flex; + align-items: center; + padding-right: 8px; } -.llmButtonIconBackgroundCopy { - background-image: url('https://docs.apify.com/img/copy.svg'); +.menuOption { + padding-block: 4px; + display: flex; + gap: 4px; } -/* Dark theme adjustments */ -[data-theme='dark'] .llmButtonIcon { - color: #e0e0e0; +.menuOptionIcon { + flex-shrink: 0; + margin-top: 2px; } -[data-theme='dark'] .llmButtonIconBackgroundMarkdown { - background-image: url('https://docs.apify.com/img/markdown-dark-theme.svg'); +.menuOptionText { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; } -[data-theme='dark'] .llmButtonIconBackgroundCopy { - background-image: url('https://docs.apify.com/img/copy-dark-theme.svg'); +.menuOptionExternalIcon { + flex-shrink: 0; + margin-top: 2px; } diff --git a/package-lock.json b/package-lock.json index 5f2873b342..0ec246a30e 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.18.3", + "@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.18.3", + "resolved": "https://registry.npmjs.org/@apify/ui-icons/-/ui-icons-1.18.3.tgz", + "integrity": "sha512-A6kLj6qn2EmXECOvlCnF807JRGzDLxhAszKx5CCg7hLJPuJXZz/4NyWmKq6gfpCGJykqu2NhnsZRKXgsgDymRg==", "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..321b31a02c 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.18.3", "@docusaurus/core": "^3.8.1", "@docusaurus/faster": "^3.8.1", "@docusaurus/plugin-client-redirects": "^3.8.1", From e02ef0198fb706304e8cb929113fd2dca9f4332a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20W=C3=A9br?= Date: Tue, 30 Sep 2025 16:09:38 +0200 Subject: [PATCH 02/18] fix --- .../src/theme/LLMButtons/index.jsx | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/apify-docs-theme/src/theme/LLMButtons/index.jsx b/apify-docs-theme/src/theme/LLMButtons/index.jsx index 34c9e96806..48b6ac739f 100644 --- a/apify-docs-theme/src/theme/LLMButtons/index.jsx +++ b/apify-docs-theme/src/theme/LLMButtons/index.jsx @@ -84,31 +84,28 @@ export default function LLMButtons() { return ( { - console.log(props); - return ( -
- - - {isCopyingLoading - ? 'Copying...' - : isCopied - ? 'Copied!' - : 'Copy for LLM'} - - -
- ); - }, + MenuBase: ({ children, ref, ...props }) => ( +
+ + + {isCopyingLoading + ? 'Copying...' + : isCopied + ? 'Copied!' + : 'Copy for LLM'} + + +
+ ), }} onSelect={onMenuOptionClick} options={[ From 9c3bcd0333aff9d1afe61916520149b9798fec9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20W=C3=A9br?= Date: Tue, 30 Sep 2025 16:18:06 +0200 Subject: [PATCH 03/18] lint fixes --- .../src/theme/DocItemContent/index.js | 1 + .../src/theme/LLMButtons/index.jsx | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/apify-docs-theme/src/theme/DocItemContent/index.js b/apify-docs-theme/src/theme/DocItemContent/index.js index 8639b5ecf8..3372e7d897 100644 --- a/apify-docs-theme/src/theme/DocItemContent/index.js +++ b/apify-docs-theme/src/theme/DocItemContent/index.js @@ -6,6 +6,7 @@ import LLMButtons from '@theme/LLMButtons'; import MDXContent from '@theme/MDXContent'; import clsx from 'clsx'; import React from 'react'; + import styles from './styles.module.css'; function useSyntheticTitle() { diff --git a/apify-docs-theme/src/theme/LLMButtons/index.jsx b/apify-docs-theme/src/theme/LLMButtons/index.jsx index 48b6ac739f..b17e4cdfd8 100644 --- a/apify-docs-theme/src/theme/LLMButtons/index.jsx +++ b/apify-docs-theme/src/theme/LLMButtons/index.jsx @@ -1,13 +1,24 @@ import React, { useState } from 'react'; -import styles from './styles.module.css'; -import { Menu, Text, theme, PlainMenuBaseComponent } from '@apify/ui-library'; import { + ChevronDownIcon, CopyIcon, ExternalLinkIcon, MarkdownIcon, - ChevronDownIcon, } from '@apify/ui-icons'; +import { Menu, Text, theme } from '@apify/ui-library'; + +import styles from './styles.module.css'; + +function ButtonText({ isLoading, isCopied }) { + if (isLoading) { + return 'Copying...'; + } + if (isCopied) { + return 'Copied!'; + } + return 'Copy for LLM'; +} export default function LLMButtons() { const [isCopyingLoading, setCopyingIsLoading] = useState(false); @@ -92,11 +103,7 @@ export default function LLMButtons() { className={styles.llmButtonText} onClick={onCopyAsMarkdownClick} > - {isCopyingLoading - ? 'Copying...' - : isCopied - ? 'Copied!' - : 'Copy for LLM'} + Date: Tue, 30 Sep 2025 17:10:02 +0200 Subject: [PATCH 04/18] feat: Add LLMs chat prompt links --- .../src/theme/LLMButtons/index.jsx | 90 +++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/apify-docs-theme/src/theme/LLMButtons/index.jsx b/apify-docs-theme/src/theme/LLMButtons/index.jsx index b17e4cdfd8..f34675957d 100644 --- a/apify-docs-theme/src/theme/LLMButtons/index.jsx +++ b/apify-docs-theme/src/theme/LLMButtons/index.jsx @@ -24,6 +24,10 @@ export default function LLMButtons() { const [isCopyingLoading, setCopyingIsLoading] = useState(false); const [isCopied, setIsCopied] = useState(false); + const currentUrl = window.location.href; + const prompt = `Read from ${currentUrl} so I can ask questions about it.`; + const markdownUrl = `${currentUrl}.md`; + const onCopyAsMarkdownClick = async () => { if (window.analytics) { window.analytics.track('Clicked', { @@ -36,9 +40,6 @@ export default function LLMButtons() { try { setCopyingIsLoading(true); - const currentUrl = window.location.href; - const markdownUrl = `${currentUrl}.md`; - // Fetch the markdown content const response = await fetch(markdownUrl); @@ -71,14 +72,60 @@ export default function LLMButtons() { } try { - const currentUrl = window.location.href; - const markdownUrl = `${currentUrl}.md`; window.open(markdownUrl, '_blank'); } catch (error) { console.error('Error opening markdown file:', error); } }; + const onOpenInChatGPTClick = () => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Open in ChatGPT', + element: 'llm-buttons.openInChatGPT', + }); + } + + 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', + }); + } + + 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', + }); + } + + try { + window.open(`https://www.perplexity.ai/search/new?q=${encodeURIComponent(prompt)}`, '_blank'); + } catch (error) { + console.error('Error opening Perplexity:', error); + } + }; + const onMenuOptionClick = (value) => { switch (value) { case 'copyForLLM': @@ -87,6 +134,15 @@ export default function LLMButtons() { case 'viewAsMarkdown': onViewAsMarkdownClick(); break; + case 'openInChatGPT': + onOpenInChatGPTClick(); + break; + case 'openInClaude': + onOpenInClaudeClick(); + break; + case 'openInPerplexity': + onOpenInPerplexityClick(); + break; default: break; } @@ -130,6 +186,30 @@ export default function LLMButtons() { icon: MarkdownIcon, value: 'viewAsMarkdown', }, + { + label: 'Open in ChatGPT', + description: 'Ask questions about this page', + showExternalIcon: true, + // TODO: Replace icon once we have one + icon: MarkdownIcon, + value: 'openInChatGPT', + }, + { + label: 'Open in Claude', + description: 'Ask questions about this page', + showExternalIcon: true, + // TODO: Replace icon once we have one + icon: MarkdownIcon, + value: 'openInClaude', + }, + { + label: 'Open in Perplexity', + description: 'Ask questions about this page', + showExternalIcon: true, + // TODO: Replace icon once we have one + icon: MarkdownIcon, + value: 'openInPerplexity', + }, ]} renderOption={(option) => (
From a9f0357f8b93b248f4ecded91f52ed6cbbbee931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20W=C3=A9br?= Date: Wed, 1 Oct 2025 12:07:04 +0200 Subject: [PATCH 05/18] cleaner code --- .../theme/DocItemContent/styles.module.css | 4 +- .../src/theme/LLMButtons/index.jsx | 390 ++++++++++-------- .../src/theme/LLMButtons/styles.module.css | 55 ++- 3 files changed, 255 insertions(+), 194 deletions(-) diff --git a/apify-docs-theme/src/theme/DocItemContent/styles.module.css b/apify-docs-theme/src/theme/DocItemContent/styles.module.css index b54a07f28b..cb2cd3e57a 100644 --- a/apify-docs-theme/src/theme/DocItemContent/styles.module.css +++ b/apify-docs-theme/src/theme/DocItemContent/styles.module.css @@ -1,8 +1,6 @@ .docItemContent { display: flex; - flex-direction: row; - justify-content: space-between; align-items: center; - column-gap: 12px; + column-gap: 1.2rem; flex-wrap: wrap; } diff --git a/apify-docs-theme/src/theme/LLMButtons/index.jsx b/apify-docs-theme/src/theme/LLMButtons/index.jsx index f34675957d..643090f0ad 100644 --- a/apify-docs-theme/src/theme/LLMButtons/index.jsx +++ b/apify-docs-theme/src/theme/LLMButtons/index.jsx @@ -10,138 +10,239 @@ import { Menu, Text, theme } from '@apify/ui-library'; import styles from './styles.module.css'; -function ButtonText({ isLoading, isCopied }) { - if (isLoading) { - return 'Copying...'; - } - if (isCopied) { - return 'Copied!'; +const DROPDOWN_OPTIONS = [ + { + label: 'Copy page', + 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, + // TODO: Replace icon once we have one + Icon: MarkdownIcon, + value: 'openInChatGPT', + }, + { + label: 'Open in Claude', + description: 'Ask questions about this page', + showExternalIcon: true, + // TODO: Replace icon once we have one + Icon: MarkdownIcon, + value: 'openInClaude', + }, + { + label: 'Open in Perplexity', + description: 'Ask questions about this page', + showExternalIcon: true, + // TODO: Replace icon once we have one + Icon: MarkdownIcon, + value: 'openInPerplexity', + }, +]; + +const onOpenInChatGPTClick = ({ prompt }) => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Open in ChatGPT', + element: 'llm-buttons.openInChatGPT', + }); } - return 'Copy for LLM'; -} -export default function LLMButtons() { - const [isCopyingLoading, setCopyingIsLoading] = useState(false); - const [isCopied, setIsCopied] = useState(false); + try { + window.open( + `https://chatgpt.com/?hints=search&q=${encodeURIComponent(prompt)}`, + '_blank', + ); + } catch (error) { + console.error('Error opening ChatGPT:', error); + } +}; - const currentUrl = window.location.href; - const prompt = `Read from ${currentUrl} so I can ask questions about it.`; - const markdownUrl = `${currentUrl}.md`; +const onOpenInClaudeClick = ({ prompt }) => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Open in Claude', + element: 'llm-buttons.openInClaude', + }); + } - const onCopyAsMarkdownClick = async () => { - if (window.analytics) { - window.analytics.track('Clicked', { - app: 'docs', - button_text: 'Copy for LLM', - element: 'llm-buttons.copyForLLM', - }); - } + try { + window.open( + `https://claude.ai/new?q=${encodeURIComponent(prompt)}`, + '_blank', + ); + } catch (error) { + console.error('Error opening Claude:', error); + } +}; - try { - setCopyingIsLoading(true); +const onOpenInPerplexityClick = ({ prompt }) => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Open in Perplexity', + element: 'llm-buttons.openInPerplexity', + }); + } - // Fetch the markdown content - const response = await fetch(markdownUrl); + try { + window.open( + `https://www.perplexity.ai/search/new?q=${encodeURIComponent( + prompt, + )}`, + '_blank', + ); + } catch (error) { + console.error('Error opening Perplexity:', error); + } +}; - if (!response.ok) { - throw new Error(`Failed to fetch markdown: ${response.status}`); - } +const onCopyAsMarkdownClick = async ({ setCopyingStatus, markdownUrl }) => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'Copy for LLM', + element: 'llm-buttons.copyForLLM', + }); + } - const markdownContent = await response.text(); + try { + setCopyingStatus('loading'); - // Copy to clipboard - await navigator.clipboard.writeText(markdownContent); + // Fetch the markdown content + const response = await fetch(markdownUrl); - // Show success feedback - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - } catch (error) { - console.error('Failed to copy markdown content:', error); - } finally { - setCopyingIsLoading(false); + if (!response.ok) { + throw new Error(`Failed to fetch markdown: ${response.status}`); } - }; - const onViewAsMarkdownClick = () => { - if (window.analytics) { - window.analytics.track('Clicked', { - app: 'docs', - button_text: 'View as Markdown', - element: 'llm-buttons.viewAsMarkdown', - }); - } + const markdownContent = await response.text(); - try { - window.open(markdownUrl, '_blank'); - } catch (error) { - console.error('Error opening markdown file:', error); - } - }; + // Copy to clipboard + await navigator.clipboard.writeText(markdownContent); - const onOpenInChatGPTClick = () => { - if (window.analytics) { - window.analytics.track('Clicked', { - app: 'docs', - button_text: 'Open in ChatGPT', - element: 'llm-buttons.openInChatGPT', - }); - } + // Show success feedback + setCopyingStatus('copied'); + setTimeout(() => setCopyingStatus('idle'), 2000); + } catch (error) { + console.error('Failed to copy markdown content:', error); + } finally { + setCopyingStatus('idle'); + } +}; - try { - window.open(`https://chatgpt.com/?hints=search&q=${encodeURIComponent(prompt)}`, '_blank'); - } catch (error) { - console.error('Error opening ChatGPT:', error); - } - }; +const onViewAsMarkdownClick = ({ markdownUrl }) => { + if (window.analytics) { + window.analytics.track('Clicked', { + app: 'docs', + button_text: 'View as Markdown', + element: 'llm-buttons.viewAsMarkdown', + }); + } - const onOpenInClaudeClick = () => { - if (window.analytics) { - window.analytics.track('Clicked', { - app: 'docs', - button_text: 'Open in Claude', - element: 'llm-buttons.openInClaude', - }); - } + try { + window.open(markdownUrl, '_blank'); + } catch (error) { + console.error('Error opening markdown file:', error); + } +}; - try { - window.open(`https://claude.ai/new?q=${encodeURIComponent(prompt)}`, '_blank'); - } catch (error) { - console.error('Error opening Claude:', error); - } - }; +function getButtonText({ status }) { + switch (status) { + case 'loading': + return 'Copying...'; + case 'copied': + return 'Copied!'; + default: + return 'Copy for LLM'; + } +} - const onOpenInPerplexityClick = () => { - if (window.analytics) { - window.analytics.track('Clicked', { - app: 'docs', - button_text: 'Open in Perplexity', - element: 'llm-buttons.openInPerplexity', - }); - } +const MenuBase = ({ + children, + ref, + copyingStatus, + setCopyingStatus, + markdownUrl, + ...props +}) => ( +
+
+
onCopyAsMarkdownClick({ setCopyingStatus, markdownUrl }) + } + > + +
+ onCopyAsMarkdownClick({ setCopyingStatus, markdownUrl }) + } + className={styles.llmButtonText} + > + {getButtonText({ status: copyingStatus })} + +
+ +
+
+
+); - try { - window.open(`https://www.perplexity.ai/search/new?q=${encodeURIComponent(prompt)}`, '_blank'); - } catch (error) { - console.error('Error opening Perplexity:', error); - } - }; +const Option = ({ Icon, label, description, showExternalIcon }) => ( +
+ +
+ {label} + + {description} + +
+ {showExternalIcon && ( + + )} +
+); +export default function LLMButtons() { + const [copyingStatus, setCopyingStatus] = useState('idle'); + + const currentUrl = window.location.href; + const prompt = `Read from ${currentUrl} so I can ask questions about it.`; + const markdownUrl = `${currentUrl}.md`; const onMenuOptionClick = (value) => { switch (value) { case 'copyForLLM': - onCopyAsMarkdownClick(); + onCopyAsMarkdownClick({ setCopyingStatus, markdownUrl }); break; case 'viewAsMarkdown': - onViewAsMarkdownClick(); + onViewAsMarkdownClick({ markdownUrl }); break; case 'openInChatGPT': - onOpenInChatGPTClick(); + onOpenInChatGPTClick({ prompt }); break; case 'openInClaude': - onOpenInClaudeClick(); + onOpenInClaudeClick({ prompt }); break; case 'openInPerplexity': - onOpenInPerplexityClick(); + onOpenInPerplexityClick({ prompt }); break; default: break; @@ -150,87 +251,20 @@ export default function LLMButtons() { return ( ( -
- - - - - -
+ MenuBase: (props) => ( + ), }} onSelect={onMenuOptionClick} - options={[ - { - label: 'Copy page', - 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, - // TODO: Replace icon once we have one - icon: MarkdownIcon, - value: 'openInChatGPT', - }, - { - label: 'Open in Claude', - description: 'Ask questions about this page', - showExternalIcon: true, - // TODO: Replace icon once we have one - icon: MarkdownIcon, - value: 'openInClaude', - }, - { - label: 'Open in Perplexity', - description: 'Ask questions about this page', - showExternalIcon: true, - // TODO: Replace icon once we have one - icon: MarkdownIcon, - value: 'openInPerplexity', - }, - ]} - renderOption={(option) => ( -
- -
- {option.label} - - {option.description} - -
- {option.showExternalIcon && ( - - )} -
- )} + options={DROPDOWN_OPTIONS} + renderOption={(props) =>