|
1 | 1 | 'use client' |
2 | 2 |
|
3 | | -import { Chip } from '@sim/emcn' |
| 3 | +import { useRef, useState } from 'react' |
| 4 | +import { Chip, useCopyToClipboard } from '@sim/emcn' |
4 | 5 | import { Check, Duplicate } from '@sim/emcn/icons' |
5 | | -import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button' |
6 | 6 |
|
7 | | -export function LLMCopyButton({ content }: { content: string }) { |
8 | | - const [checked, onClick] = useCopyButton(() => navigator.clipboard.writeText(content)) |
| 7 | +interface LLMCopyButtonProps { |
| 8 | + markdownUrl: string |
| 9 | +} |
| 10 | + |
| 11 | +export function LLMCopyButton({ markdownUrl }: LLMCopyButtonProps) { |
| 12 | + const { copied, copy } = useCopyToClipboard() |
| 13 | + const [status, setStatus] = useState<'idle' | 'pending' | 'ready' | 'failed'>('idle') |
| 14 | + const preparedMarkdown = useRef<string | null>(null) |
| 15 | + const pending = status === 'pending' |
| 16 | + const checked = copied && status === 'idle' |
| 17 | + |
| 18 | + async function onClick() { |
| 19 | + setStatus('pending') |
| 20 | + const prepare = async () => { |
| 21 | + const response = await fetch(markdownUrl, { |
| 22 | + signal: typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(10000) : undefined, |
| 23 | + }) |
| 24 | + if (!response.ok) throw new Error('Could not load page Markdown') |
| 25 | + const markdown = await response.text() |
| 26 | + if (!markdown.trim()) throw new Error('Page Markdown is empty') |
| 27 | + return markdown |
| 28 | + } |
| 29 | + try { |
| 30 | + // Promise-backed writes preserve Safari's transient user activation during the fetch. |
| 31 | + if (typeof ClipboardItem !== 'undefined' && typeof navigator.clipboard.write === 'function') { |
| 32 | + const success = await copy({ prepare, fallback: '' }) |
| 33 | + setStatus(success ? 'idle' : 'failed') |
| 34 | + return |
| 35 | + } |
| 36 | + if (preparedMarkdown.current === null) { |
| 37 | + preparedMarkdown.current = await prepare() |
| 38 | + setStatus('ready') |
| 39 | + return |
| 40 | + } |
| 41 | + const success = await copy(preparedMarkdown.current) |
| 42 | + setStatus(success ? 'idle' : 'failed') |
| 43 | + } catch { |
| 44 | + setStatus('failed') |
| 45 | + } |
| 46 | + } |
9 | 47 |
|
10 | 48 | return ( |
11 | | - <Chip |
12 | | - onClick={onClick} |
13 | | - leftIcon={checked ? Check : Duplicate} |
14 | | - aria-label={checked ? 'Copied to clipboard' : 'Copy page content'} |
15 | | - > |
16 | | - {checked ? 'Copied' : 'Copy page'} |
17 | | - </Chip> |
| 49 | + <div className='relative'> |
| 50 | + <Chip |
| 51 | + onClick={onClick} |
| 52 | + disabled={pending} |
| 53 | + aria-busy={pending} |
| 54 | + leftIcon={checked ? Check : Duplicate} |
| 55 | + aria-label={checked ? 'Copied to clipboard' : 'Copy page content'} |
| 56 | + > |
| 57 | + {checked ? 'Copied' : pending ? 'Copying…' : 'Copy page'} |
| 58 | + </Chip> |
| 59 | + <span role='status' className='absolute top-full right-0 whitespace-nowrap text-caption'> |
| 60 | + {status === 'failed' |
| 61 | + ? 'Could not copy. Try again.' |
| 62 | + : status === 'ready' |
| 63 | + ? 'Click again to copy.' |
| 64 | + : ''} |
| 65 | + </span> |
| 66 | + </div> |
18 | 67 | ) |
19 | 68 | } |
0 commit comments