diff --git a/app/(docs)/[[...slug]]/page.tsx b/app/(docs)/[[...slug]]/page.tsx index 96d717cf..1231e1a5 100644 --- a/app/(docs)/[[...slug]]/page.tsx +++ b/app/(docs)/[[...slug]]/page.tsx @@ -18,6 +18,7 @@ import { import { notFound } from "next/navigation"; import { CommunityButton } from "../../../components/Community"; import { NavButton } from "../../../components/NavButton"; +import CopyPageDropdown from "../../../components/CopyPageDropdown"; export default async function Page(props: { params: Promise<{ slug?: string[] }>; @@ -40,7 +41,12 @@ export default async function Page(props: { path: `content/${page.file.path}`, }} > - {page.data.title} +
+ {page.data.title} +
+ +
+
{page.data.description} - ), }, diff --git a/components/CopyPageDropdown.tsx b/components/CopyPageDropdown.tsx index d140bec9..d4b0fbe1 100644 --- a/components/CopyPageDropdown.tsx +++ b/components/CopyPageDropdown.tsx @@ -1,9 +1,11 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { usePathname } from 'next/navigation'; -import { Copy, ExternalLink, FileText, MessageSquare } from 'lucide-react'; +import { Copy, FileText, ChevronDown } from 'lucide-react'; import * as Popover from '@radix-ui/react-popover'; +import { OpenAIIcon } from './icons/OpenAIIcon'; +import { ClaudeIcon } from './icons/ClaudeIcon'; interface PageContent { content: string; @@ -12,11 +14,50 @@ interface PageContent { path: string; } -export default function CopyPageDropdown() { +type ActionType = 'copy-markdown' | 'view-markdown' | 'open-chatgpt' | 'open-claude'; + +interface ActionConfig { + id: ActionType; + label: string; + icon: React.ComponentType<{ className?: string }>; + handler: () => Promise; +} + +interface CopyPageDropdownProps { + enhanced?: boolean; +} + +const STORAGE_KEY = 'agentuity-copy-preference'; + +export default function CopyPageDropdown({ enhanced = false }: CopyPageDropdownProps) { const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [preferredAction, setPreferredAction] = useState('copy-markdown'); + const [isInitialized, setIsInitialized] = useState(false); const pathname = usePathname(); + useEffect(() => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored && ['copy-markdown', 'view-markdown', 'open-chatgpt', 'open-claude'].includes(stored)) { + setPreferredAction(stored as ActionType); + } + } catch (error) { + console.error('Failed to load copy preference:', error); + } finally { + setIsInitialized(true); + } + }, []); + + const savePreference = (action: ActionType) => { + try { + localStorage.setItem(STORAGE_KEY, action); + setPreferredAction(action); + } catch (error) { + console.error('Failed to save copy preference:', error); + } + }; + const formatMarkdownForLLM = (content: PageContent): string => { return `# ${content.title}\n\n${content.description ? `${content.description}\n\n` : ''}${content.content}`; }; @@ -45,9 +86,36 @@ export default function CopyPageDropdown() { const markdownForLLM = formatMarkdownForLLM(content); try { - await navigator.clipboard.writeText(markdownForLLM); + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(markdownForLLM); + } else { + const textArea = document.createElement('textarea'); + textArea.value = markdownForLLM; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + } } catch (error) { console.error('Failed to copy to clipboard:', error); + const textArea = document.createElement('textarea'); + textArea.value = markdownForLLM; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + } catch (fallbackError) { + console.error('Fallback copy also failed:', fallbackError); + } + document.body.removeChild(textArea); } setIsOpen(false); }; @@ -59,79 +127,163 @@ export default function CopyPageDropdown() { const markdownForLLM = formatMarkdownForLLM(content); const blob = new Blob([markdownForLLM], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); - window.open(url, '_blank'); - setTimeout(() => URL.revokeObjectURL(url), 100); + + try { + const newWindow = window.open(url, '_blank'); + if (!newWindow) { + const link = document.createElement('a'); + link.href = url; + link.download = `${content.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.md`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } catch (error) { + console.error('Failed to open markdown view:', error); + const link = document.createElement('a'); + link.href = url; + link.download = `${content.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.md`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + setTimeout(() => URL.revokeObjectURL(url), 1000); setIsOpen(false); }; const handleOpenInChatGPT = async () => { - const content = await fetchPageContent(); - if (!content) return; + const currentUrl = `${window.location.origin}${pathname}`; + const chatGPTUrl = `https://chatgpt.com/?hints=search&prompt=${encodeURIComponent(`Read from ${currentUrl} so I can ask questions about it`)}`; - const markdownForLLM = formatMarkdownForLLM(content); - const chatGPTUrl = `https://chatgpt.com/?q=${encodeURIComponent(`Please help me understand this documentation:\n\n${markdownForLLM}`)}`; - window.open(chatGPTUrl, '_blank'); + try { + const newWindow = window.open(chatGPTUrl, '_blank'); + if (!newWindow) { + window.location.href = chatGPTUrl; + } + } catch (error) { + console.error('Failed to open ChatGPT:', error); + window.location.href = chatGPTUrl; + } setIsOpen(false); }; const handleOpenInClaude = async () => { - const content = await fetchPageContent(); - if (!content) return; + const currentUrl = `${window.location.origin}${pathname}`; + const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(`Read from ${currentUrl} so I can ask questions about it`)}`; - const markdownForLLM = formatMarkdownForLLM(content); - const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(`Please help me understand this documentation:\n\n${markdownForLLM}`)}`; - window.open(claudeUrl, '_blank'); + try { + const newWindow = window.open(claudeUrl, '_blank'); + if (!newWindow) { + window.location.href = claudeUrl; + } + } catch (error) { + console.error('Failed to open Claude:', error); + window.location.href = claudeUrl; + } setIsOpen(false); }; + const actionConfigs: ActionConfig[] = [ + { + id: 'copy-markdown', + label: 'Copy as Markdown', + icon: Copy, + handler: handleCopyMarkdown + }, + { + id: 'view-markdown', + label: 'View as Markdown', + icon: FileText, + handler: handleViewMarkdown + }, + { + id: 'open-chatgpt', + label: 'Open in ChatGPT', + icon: OpenAIIcon, + handler: handleOpenInChatGPT + }, + { + id: 'open-claude', + label: 'Open in Claude', + icon: ClaudeIcon, + handler: handleOpenInClaude + } + ]; + + const primaryAction = actionConfigs.find(action => action.id === preferredAction) || actionConfigs[0]; + + const handlePrimaryAction = async () => { + await primaryAction.handler(); + }; + + const handleActionSelect = async (actionId: ActionType) => { + savePreference(actionId); + const action = actionConfigs.find(a => a.id === actionId); + if (action) { + await action.handler(); + } + }; + + if (!isInitialized) { + return null; + } + return ( - - - - -
+ {enhanced ? ( +
- - + + + +
+ ) : ( + + + )} + e.preventDefault()} + > +
+ {actionConfigs.map((action) => ( + + ))}
diff --git a/components/icons/ClaudeIcon.tsx b/components/icons/ClaudeIcon.tsx new file mode 100644 index 00000000..513cbe8d --- /dev/null +++ b/components/icons/ClaudeIcon.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +interface ClaudeIconProps { + className?: string; + size?: number; +} + +export function ClaudeIcon({ className = "w-5 h-5", size }: ClaudeIconProps) { + return ( + + Claude + + + ); +} diff --git a/components/icons/OpenAIIcon.tsx b/components/icons/OpenAIIcon.tsx new file mode 100644 index 00000000..c1d02458 --- /dev/null +++ b/components/icons/OpenAIIcon.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +interface OpenAIIconProps { + className?: string; + size?: number; +} + +export function OpenAIIcon({ className = "w-5 h-5", size }: OpenAIIconProps) { + return ( + + OpenAI + + + ); +}