Add a "Copy to LLM" dropdown with ChatGPT and Claude shortcuts (#2952)

Co-authored-by: codecalm <codecalm@gmail.com>
This commit is contained in:
Bartosz-Doandcodecalm authored and GitHub committed 2026-09-07 23:32:25 +02:00
1 parent b4b10f924a
commit 8fc079d26c
15 files changed
+313 -77

No files matched your search

+15 -2
View File
@@ -1,14 +1,26 @@
---
import MarkdownSource from './MarkdownSource.astro'
import { markdownTable } from '@shared/lib/markdown-source'
interface Props {
colors: Record<string, { prop: string; title: string }>
colors: Record<string, { prop: string; title: string; hex?: string }>
}
const { colors }: Props = Astro.props
// The .md mirror gets the palette as a table; hex values are listed only when
// the data has real ones (the gray scale points at a CSS variable instead).
const list = Object.values(colors)
const withHex = list.some((color) => color.hex?.startsWith('#'))
const markdown = markdownTable(
['Color', 'CSS variable', ...(withHex ? ['Hex'] : [])],
list.map((color) => [color.title, `\`${color.prop}\``, ...(withHex ? [color.hex?.startsWith('#') ? `\`${color.hex}\`` : ''] : [])]),
)
---
<div class="row row-cols-4 row-cols-md-6 g-3 g-md-4">
{
Object.values(colors).map((color) => {
list.map((color) => {
return (
<div class="text-center">
<div class="p-6 rounded border" style={{ backgroundColor: `var(${color.prop})` }} />
@@ -18,3 +30,4 @@ const { colors }: Props = Astro.props
})
}
</div>
<MarkdownSource name="Colors" markdown={markdown} />
@@ -0,0 +1,91 @@
---
// Fetches this page's Markdown mirror (its url with ".md" appended) and copies
// its content to the clipboard, so the page can be pasted into an LLM directly.
import Icon from '@ui/Icon.astro'
interface Props {
/** Url of the page's `.md` mirror, fetched on click. */
url: string
/** Absolute url of the same `.md` mirror, for the "Ask ChatGPT" / "Ask Claude" prompts. */
absoluteUrl?: string | undefined
class?: string
}
const { url, absoluteUrl, class: className } = Astro.props
// Both ChatGPT and Claude open with this text already in the composer; the
// reader still has to press send.
const prompt = absoluteUrl && `Read ${absoluteUrl} so I can ask questions about it.`
const askChatGptUrl = prompt && `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`
const askClaudeUrl = prompt && `https://claude.ai/new?q=${encodeURIComponent(prompt)}`
---
<div class="btn-group">
<button type="button" class:list={['btn btn-sm', className]} data-docs-copy-markdown-link={url}>
<Icon name="clipboard" />
<Icon name="check" class="d-none" />
<span data-docs-copy-markdown-link-label>Copy to LLM</span>
</button>
<button type="button" class="btn btn-sm dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href={url} target="_blank">
<Icon name="external-link" />
<span>View as markdown</span>
</a>
{
askChatGptUrl && (
<a class="dropdown-item" href={askChatGptUrl} target="_blank" rel="noopener noreferrer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
<span>Ask ChatGPT</span>
</a>
)
}
{
askClaudeUrl && (
<a class="dropdown-item" href={askClaudeUrl} target="_blank" rel="noopener noreferrer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
<path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z" />
</svg>
<span>Ask Claude</span>
</a>
)
}
</div>
</div>
<script>
// Wire every instance on the page and read each one's url from its own attribute.
const buttons = document.querySelectorAll<HTMLButtonElement>('[data-docs-copy-markdown-link]')
buttons.forEach((button) => {
const label = button.querySelector<HTMLElement>('[data-docs-copy-markdown-link-label]')
const [clipboardIcon, checkIcon] = button.querySelectorAll<HTMLElement>('svg')
button.addEventListener('click', async () => {
const url = button.dataset.docsCopyMarkdownLink
if (!url || !label) return
try {
const response = await fetch(url)
await navigator.clipboard.writeText(await response.text())
const original = label.textContent
label.textContent = 'Copied'
clipboardIcon?.classList.add('d-none')
checkIcon?.classList.remove('d-none')
setTimeout(() => {
label.textContent = original
clipboardIcon?.classList.remove('d-none')
checkIcon?.classList.add('d-none')
}, 2000)
} catch {
// clipboard permission refused — leave the label alone
}
})
})
</script>
+2 -38
View File
@@ -19,11 +19,9 @@ interface Props {
editUrl?: string | undefined
/** URL of the file implementing the component, from the page's `source` front matter */
sourceUrl?: string | undefined
/** url of this page's markdown mirror, offered as a copy button */
markdownUrl?: string | undefined
}
const { toc = [], editUrl, sourceUrl, markdownUrl } = Astro.props
const { toc = [], editUrl, sourceUrl } = Astro.props
const illustrationsCount = illustrations.length
---
@@ -54,15 +52,8 @@ const illustrationsCount = illustrations.length
</a>
{
(markdownUrl || sourceUrl || editUrl) && (
(sourceUrl || editUrl) && (
<nav class="docs-page-links mt-6" aria-label="Page tools">
{markdownUrl && (
<button type="button" class="docs-page-link" data-docs-copy-markdown={markdownUrl}>
<Icon name="clipboard" />
<span data-docs-copy-label>Copy page as Markdown</span>
</button>
)}
{sourceUrl && (
<a href={sourceUrl} class="docs-page-link" target="_blank" rel="noopener noreferrer">
<Icon name="code" />
@@ -80,30 +71,3 @@ const illustrationsCount = illustrations.length
)
}
<!-- END DOCS TOC -->
<script>
// Fetches the .md mirror of the page and puts it on the clipboard, so the
// whole page can be pasted into an assistant without hunting for the url.
const button = document.querySelector<HTMLButtonElement>('[data-docs-copy-markdown]')
if (button) {
const label = button.querySelector<HTMLElement>('[data-docs-copy-label]')
button.addEventListener('click', async () => {
if (!label) return
const url = button.dataset.docsCopyMarkdown
if (!url) return
try {
const response = await fetch(url)
await navigator.clipboard.writeText(await response.text())
const original = label.textContent
label.textContent = 'Copied'
setTimeout(() => {
label.textContent = original
}, 2000)
} catch {
// clipboard or network refused — leave the label alone
}
})
}
</script>
+9
View File
@@ -1,5 +1,13 @@
---
import MarkdownSource from './MarkdownSource.astro'
import { markdownTable } from '@shared/lib/markdown-source'
import flags from '@data/flags.json'
// Same list as a table for the page's .md mirror
const markdown = markdownTable(
['Country', 'Code', 'CSS class'],
flags.map((flag) => [flag.name, `\`${flag.flag}\``, `\`.flag-country-${flag.flag}\``]),
)
---
<table class="table table-vcenter">
@@ -30,3 +38,4 @@ import flags from '@data/flags.json'
}
</tbody>
</table>
<MarkdownSource name="Flags" markdown={markdown} />
+17
View File
@@ -0,0 +1,17 @@
---
// Ships a markdown version of a component's output for the page's .md mirror.
// Render it next to the html; `name` is the tag the MDX uses for the component.
// Only the mirror render (docs/lib/llms.ts, which sets the `markdownMirror`
// local) gets the comment — a browser page never carries it.
// See shared/lib/markdown-source.ts.
import { markdownSource } from '@shared/lib/markdown-source'
interface Props {
name: string
markdown: string
}
const { name, markdown } = Astro.props
---
{Astro.locals.markdownMirror && <Fragment set:html={markdownSource(name, markdown)} />}
+9
View File
@@ -1,5 +1,13 @@
---
import MarkdownSource from './MarkdownSource.astro'
import { markdownTable } from '@shared/lib/markdown-source'
import payments from '@data/payments.json'
// Same list as a table for the page's .md mirror
const markdown = markdownTable(
['Provider', 'CSS class', 'Dark variant'],
payments.map((payment) => [payment.name, `\`.payment-provider-${payment.logo}\``, `\`.payment-provider-${payment.logo}-dark\``]),
)
---
<table class="table table-vcenter">
@@ -36,3 +44,4 @@ import payments from '@data/payments.json'
}
</tbody>
</table>
<MarkdownSource name="Payments" markdown={markdown} />
+9
View File
@@ -1,5 +1,13 @@
---
import MarkdownSource from './MarkdownSource.astro'
import { markdownTable } from '@shared/lib/markdown-source'
import socials from '@data/socials.json'
// Same list as a table for the page's .md mirror
const markdown = markdownTable(
['App', 'CSS class'],
socials.map((social) => [social.name, `\`.social-app-${social.file}\``]),
)
---
<table class="table table-vcenter">
@@ -29,3 +37,4 @@ import socials from '@data/socials.json'
}
</tbody>
</table>
<MarkdownSource name="Socials" markdown={markdown} />