diff --git a/website/package.json b/website/package.json
index 1597a95f46..3b158d5c30 100644
--- a/website/package.json
+++ b/website/package.json
@@ -18,6 +18,7 @@
"devDependencies": {
"@apify/eslint-config-ts": "^0.4.0",
"@apify/tsconfig": "^0.1.0",
+ "@apify/ui-icons": "^1.23.0",
"@docusaurus/module-type-aliases": "3.9.1",
"@docusaurus/types": "3.9.1",
"@types/react": "^19.0.0",
diff --git a/website/src/components/LLMButtons.jsx b/website/src/components/LLMButtons.jsx
new file mode 100644
index 0000000000..80f36442ec
--- /dev/null
+++ b/website/src/components/LLMButtons.jsx
@@ -0,0 +1,510 @@
+import {
+ AnthropicIcon,
+ ChatGptIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ CopyIcon,
+ ExternalLinkIcon,
+ LoaderIcon,
+ MarkdownIcon,
+ PerplexityIcon,
+} from '@apify/ui-icons';
+import { useLocation } from '@docusaurus/router';
+import clsx from 'clsx';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+
+import styles from './LLMButtons.module.css';
+
+const DROPDOWN_OPTIONS = [
+ {
+ label: 'Copy for LLM',
+ description: 'Copy page as Markdown for LLMs',
+ showExternalIcon: false,
+ icon: CopyIcon,
+ value: 'copyForLLM',
+ analytics: {
+ buttonText: 'Copy for LLM',
+ element: 'llm-buttons.copyForLLM',
+ },
+ },
+ {
+ label: 'View as Markdown',
+ description: 'View this page as plain text',
+ icon: MarkdownIcon,
+ value: 'viewAsMarkdown',
+ showExternalIcon: true,
+ analytics: {
+ buttonText: 'View as Markdown',
+ element: 'llm-buttons.viewAsMarkdown',
+ },
+ },
+ {
+ label: 'Open in ChatGPT',
+ description: 'Ask questions about this page',
+ icon: ChatGptIcon,
+ value: 'openInChatGPT',
+ showExternalIcon: true,
+ analytics: {
+ buttonText: 'Open in ChatGPT',
+ element: 'llm-buttons.openInChatGPT',
+ },
+ },
+ {
+ label: 'Open in Claude',
+ description: 'Ask questions about this page',
+ icon: AnthropicIcon,
+ value: 'openInClaude',
+ showExternalIcon: true,
+ analytics: {
+ buttonText: 'Open in Claude',
+ element: 'llm-buttons.openInClaude',
+ },
+ },
+ {
+ label: 'Open in Perplexity',
+ description: 'Ask questions about this page',
+ icon: PerplexityIcon,
+ value: 'openInPerplexity',
+ showExternalIcon: true,
+ analytics: {
+ buttonText: 'Open in Perplexity',
+ element: 'llm-buttons.openInPerplexity',
+ },
+ },
+];
+
+const CHAT_GPT_BASE = 'https://chatgpt.com/?hints=search&q=';
+const CLAUDE_BASE = 'https://claude.ai/new?q=';
+const PERPLEXITY_BASE = 'https://www.perplexity.ai/search/new?q=';
+
+const getPrompt = (currentUrl) => `Read from ${currentUrl} so I can ask questions about it.`;
+const getMarkdownUrl = (currentUrl) => {
+ const url = new URL(currentUrl);
+ url.pathname = `${url.pathname.replace(/\/$/, '')}.md`;
+ return url.toString();
+};
+
+const trackClick = (buttonText, element) => {
+ if (typeof window !== 'undefined' && window.analytics) {
+ window.analytics.track('Clicked', {
+ app: 'crawlee',
+ button_text: buttonText,
+ element,
+ });
+ }
+};
+
+const getOptionHref = (value, currentUrl) => {
+ if (!currentUrl) {
+ return undefined;
+ }
+
+ switch (value) {
+ case 'viewAsMarkdown':
+ return getMarkdownUrl(currentUrl);
+ case 'openInChatGPT':
+ return `${CHAT_GPT_BASE}${encodeURIComponent(getPrompt(currentUrl))}`;
+ case 'openInClaude':
+ return `${CLAUDE_BASE}${encodeURIComponent(getPrompt(currentUrl))}`;
+ case 'openInPerplexity':
+ return `${PERPLEXITY_BASE}${encodeURIComponent(getPrompt(currentUrl))}`;
+ default:
+ return undefined;
+ }
+};
+
+const Menu = ({
+ className,
+ components = {},
+ onMenuOpen,
+ onSelect,
+ options = [],
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [focusedIndex, setFocusedIndex] = useState(0);
+ const menuRef = useRef(null);
+ const menuItemRefs = useRef([]);
+
+ const MenuBaseComponent = components.MenuBase;
+
+ const closeMenu = useCallback(() => {
+ setIsOpen(false);
+ setFocusedIndex(0);
+ }, []);
+
+ const toggleMenu = useCallback(() => {
+ setIsOpen((prev) => {
+ if (!prev) {
+ setFocusedIndex(0);
+ }
+ return !prev;
+ });
+ }, []);
+
+ const handleKeyDown = useCallback(
+ (event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ toggleMenu();
+ } else if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ if (!isOpen) {
+ toggleMenu();
+ } else {
+ setFocusedIndex((prev) => (prev + 1) % options.length);
+ }
+ } else if (event.key === 'ArrowUp') {
+ event.preventDefault();
+ if (isOpen) {
+ setFocusedIndex((prev) => (prev - 1 + options.length) % options.length);
+ }
+ }
+ },
+ [toggleMenu, isOpen, options.length],
+ );
+
+ const handleOptionSelect = useCallback(
+ (option, event) => {
+ onSelect?.(option, event);
+ closeMenu();
+ },
+ [closeMenu, onSelect],
+ );
+
+ const handleMenuItemKeyDown = useCallback(
+ (event, option, index) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ event.currentTarget.click();
+ return;
+ }
+
+ if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ setFocusedIndex((index + 1) % options.length);
+ return;
+ }
+
+ if (event.key === 'ArrowUp') {
+ event.preventDefault();
+ setFocusedIndex((index - 1 + options.length) % options.length);
+ return;
+ }
+
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ closeMenu();
+ }
+ },
+ [options.length, closeMenu],
+ );
+
+ useEffect(() => {
+ onMenuOpen?.(isOpen);
+ }, [isOpen, onMenuOpen]);
+
+ useEffect(() => {
+ if (isOpen && menuItemRefs.current[focusedIndex]) {
+ menuItemRefs.current[focusedIndex].focus();
+ }
+ }, [isOpen, focusedIndex]);
+
+ useEffect(() => {
+ if (!isOpen) {
+ return undefined;
+ }
+
+ const handleClickOutside = (event) => {
+ if (!menuRef.current?.contains(event.target)) {
+ closeMenu();
+ }
+ };
+
+ const handleEscape = (event) => {
+ if (event.key === 'Escape') {
+ closeMenu();
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ document.addEventListener('keydown', handleEscape);
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ document.removeEventListener('keydown', handleEscape);
+ };
+ }, [closeMenu, isOpen]);
+
+ return (
+
+
+ {isOpen && (
+
+ )}
+
+ );
+};
+
+function getButtonText({ status }) {
+ switch (status) {
+ case 'loading':
+ return 'Copying...';
+ case 'copied':
+ return 'Copied';
+ default:
+ return 'Copy for LLM';
+ }
+}
+
+const onCopyAsMarkdownClick = async ({ setCopyingStatus, currentUrl }) => {
+ const sourceUrl = currentUrl || (typeof window !== 'undefined' ? window.location.href : '');
+
+ if (!sourceUrl) {
+ return;
+ }
+
+ trackClick('Copy for LLM', 'llm-buttons.copyForLLM');
+
+ const markdownUrl = getMarkdownUrl(sourceUrl);
+
+ try {
+ setCopyingStatus('loading');
+
+ const response = await fetch(markdownUrl);
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch markdown: ${response.status}`);
+ }
+
+ const markdownContent = await response.text();
+ await navigator.clipboard.writeText(markdownContent);
+ setCopyingStatus('copied');
+ } catch (error) {
+ console.error('Failed to copy markdown content:', error);
+ } finally {
+ setTimeout(() => setCopyingStatus('idle'), 2000);
+ }
+};
+
+const COPYING_STATUS_ICON = {
+ loading: ,
+ copied: ,
+ idle: ,
+}
+
+const MenuBase = React.forwardRef(({
+ copyingStatus,
+ setCopyingStatus,
+ chevronIconRef,
+ currentUrl,
+ ...buttonProps
+}, ref) => {
+ const mergedButtonProps = {
+ ...buttonProps,
+ tabIndex: buttonProps.tabIndex ?? 0,
+ };
+
+ return (
+
+
+
{
+ event.stopPropagation();
+ onCopyAsMarkdownClick({ setCopyingStatus, currentUrl });
+ }}
+ >
+ {COPYING_STATUS_ICON[copyingStatus]}
+
+
{
+ event.stopPropagation();
+ onCopyAsMarkdownClick({ setCopyingStatus, currentUrl });
+ }}
+ className={styles.llmButtonText}
+ >
+ {getButtonText({ status: copyingStatus })}
+
+
+
+
+
+
+ );
+});
+MenuBase.displayName = 'MenuBase';
+
+const Option = ({ label, description, showExternalIcon, icon }) => {
+ const Icon = icon ?? CopyIcon;
+
+ return (
+
+
+
+ {label}
+ {description}
+
+ {showExternalIcon && (
+
+ )}
+
+ );
+};
+
+export default function LLMButtons() {
+ const [copyingStatus, setCopyingStatus] = useState('idle');
+ const [isMarkdownAvailable, setIsMarkdownAvailable] = useState(false);
+ const chevronIconRef = useRef(null);
+ const location = useLocation();
+
+ const currentUrl = typeof window !== 'undefined'
+ ? `${window.location.origin}${location.pathname}${location.search}${location.hash}`
+ : '';
+
+ useEffect(() => {
+ if (!currentUrl) {
+ // TODO: Feel free to tell me how to fix this 🤦♂️
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ setIsMarkdownAvailable(false);
+ return undefined;
+ }
+
+ const controller = new AbortController();
+ const markdownUrl = getMarkdownUrl(currentUrl);
+
+ const checkMarkdownAvailability = async () => {
+ try {
+ const response = await fetch(markdownUrl, {
+ method: 'HEAD',
+ signal: controller.signal,
+ });
+ setIsMarkdownAvailable(response.ok);
+ } catch (error) {
+ if (error.name === 'AbortError') {
+ return;
+ }
+ setIsMarkdownAvailable(false);
+ }
+ };
+
+ checkMarkdownAvailability();
+
+ return () => {
+ controller.abort();
+ };
+ }, [currentUrl]);
+
+ const menuOptions = useMemo(
+ () => DROPDOWN_OPTIONS.map((option) => {
+ const href = getOptionHref(option.value, currentUrl);
+
+ if (option.value === 'viewAsMarkdown') {
+ if (!isMarkdownAvailable) {
+ return null;
+ }
+ }
+
+ return {
+ ...option,
+ href,
+ target: href ? '_blank' : undefined,
+ rel: href ? 'noopener noreferrer' : undefined,
+ };
+ }).filter(Boolean),
+ [isMarkdownAvailable, currentUrl],
+ );
+
+ const onMenuOptionClick = useCallback(
+ (option, event) => {
+ if (!option) {
+ return;
+ }
+
+ if (option.analytics) {
+ trackClick(option.analytics.buttonText, option.analytics.element);
+ }
+
+ if (option.value === 'copyForLLM') {
+ event?.preventDefault();
+ onCopyAsMarkdownClick({ setCopyingStatus, currentUrl });
+ }
+ },
+ [setCopyingStatus, currentUrl],
+ );
+
+ return (
+