From 64fd0f0b491c38ef145a8471c3fae38d338a9f47 Mon Sep 17 00:00:00 2001 From: data-cowwboy Date: Fri, 27 Feb 2026 12:32:16 +0530 Subject: [PATCH 1/5] feat: add "Use with LLM" dropdown to doc pages Adds a dropdown button on every doc page with options to open the page in ChatGPT or Claude, copy the raw markdown source to clipboard, and view the page as plain markdown via GitHub raw URL. --- src/components/AiActionsDropdown/index.tsx | 155 ++++++++++++++++++ .../AiActionsDropdown/styles.module.css | 103 ++++++++++++ src/theme/DocItem/Layout/index.tsx | 17 ++ 3 files changed, 275 insertions(+) create mode 100644 src/components/AiActionsDropdown/index.tsx create mode 100644 src/components/AiActionsDropdown/styles.module.css create mode 100644 src/theme/DocItem/Layout/index.tsx diff --git a/src/components/AiActionsDropdown/index.tsx b/src/components/AiActionsDropdown/index.tsx new file mode 100644 index 000000000..d735cb756 --- /dev/null +++ b/src/components/AiActionsDropdown/index.tsx @@ -0,0 +1,155 @@ +import React, { useState, useRef, useEffect } from 'react' +import styles from './styles.module.css' + +// Simple inline SVG icons +const ChatGPTIcon = () => ( + + + +) + +const ClaudeIcon = () => ( + + + +) + +const CopyIcon = () => ( + + + + +) + +const MarkdownIcon = () => ( + + + +) + +const ExternalIcon = () => ( + + + +) + +interface Props { + editUrl?: string +} + +export default function AiActionsDropdown({ editUrl }: Props) { + const [open, setOpen] = useState(false) + const [copied, setCopied] = useState(false) + const ref = useRef(null) + + useEffect(() => { + const onClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', onClickOutside) + return () => document.removeEventListener('mousedown', onClickOutside) + }, []) + + const pageUrl = typeof window !== 'undefined' ? window.location.href : '' + + // Convert GitHub editUrl to raw content URL + // e.g. https://github.com/cowprotocol/docs/tree/main/docs/foo.md + // -> https://raw.githubusercontent.com/cowprotocol/docs/main/docs/foo.md + const rawUrl = editUrl + ?.replace('github.com', 'raw.githubusercontent.com') + .replace('/tree/', '/') + + const openInChatGPT = () => { + const q = encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it`) + window.open(`https://chatgpt.com/?hints=search&q=${q}`, '_blank') + setOpen(false) + } + + const openInClaude = () => { + const q = encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it`) + window.open(`https://claude.ai/new?q=${q}`, '_blank') + setOpen(false) + } + + const copyPage = async () => { + if (rawUrl) { + try { + const res = await fetch(rawUrl) + const md = await res.text() + await navigator.clipboard.writeText(md) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + // Fallback: copy article text if fetch fails + const article = document.querySelector('article') + if (article) { + await navigator.clipboard.writeText(article.innerText) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + } + setOpen(false) + } + + const viewMarkdown = () => { + if (rawUrl) { + window.open(rawUrl, '_blank') + } + setOpen(false) + } + + return ( +
+ + + {open && ( +
+ + + + + + + {editUrl && ( + + )} +
+ )} +
+ ) +} diff --git a/src/components/AiActionsDropdown/styles.module.css b/src/components/AiActionsDropdown/styles.module.css new file mode 100644 index 000000000..780b74d06 --- /dev/null +++ b/src/components/AiActionsDropdown/styles.module.css @@ -0,0 +1,103 @@ +.wrapper { + position: relative; + display: inline-block; +} + +.trigger { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border: none; + border-radius: 8px; + background: var(--ifm-color-primary); + color: #fff; + font-size: 0.85rem; + cursor: pointer; + transition: opacity 0.15s, box-shadow 0.15s; +} + +.trigger:hover { + opacity: 0.9; + box-shadow: var(--ifm-global-shadow-lw); +} + +.triggerIcon { + width: 18px; + height: 18px; + opacity: 1; +} + +.chevron { + width: 14px; + height: 14px; + opacity: 0.8; + transition: transform 0.15s; +} + +.chevronOpen { + transform: rotate(180deg); +} + +.dropdown { + position: absolute; + top: calc(100% + 4px); + right: 0; + z-index: 100; + min-width: 260px; + padding: 6px; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 10px; + background: var(--ifm-background-color); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +.item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px; + border: none; + border-radius: 8px; + background: none; + color: var(--ifm-color-emphasis-800); + font-size: 0.85rem; + text-decoration: none; + cursor: pointer; + transition: background 0.12s; +} + +.item:hover { + background: var(--ifm-background-surface-color); + color: var(--ifm-color-emphasis-900); + text-decoration: none; +} + +.itemIcon { + width: 20px; + height: 20px; + flex-shrink: 0; + opacity: 0.75; +} + +.itemText { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.itemLabel { + font-weight: 500; + line-height: 1.3; +} + +.itemDesc { + font-size: 0.75rem; + color: var(--ifm-color-emphasis-500); + line-height: 1.3; +} + +.copied .itemLabel { + color: #16a34a; +} diff --git a/src/theme/DocItem/Layout/index.tsx b/src/theme/DocItem/Layout/index.tsx new file mode 100644 index 000000000..40b93ca57 --- /dev/null +++ b/src/theme/DocItem/Layout/index.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import OriginalDocItemLayout from '@theme-original/DocItem/Layout' +import { useDoc } from '@docusaurus/plugin-content-docs/client' +import AiActionsDropdown from '@site/src/components/AiActionsDropdown' + +export default function DocItemLayout(props: React.ComponentProps) { + const { metadata } = useDoc() + + return ( + <> +
+ +
+ + + ) +} From fa49accac4eb0fd816dd0585749d2ee6783e5094 Mon Sep 17 00:00:00 2001 From: data-cowwboy Date: Fri, 27 Feb 2026 12:37:24 +0530 Subject: [PATCH 2/5] fix: dark mode styling and correct icon for LLM dropdown - Fix button colors for dark mode (light blue bg, dark text) - Use correct logo SVG for the AI assistant option - Update LLM prompt to "Read from so I can ask questions about it" - Fetch raw markdown from GitHub for copy/view actions --- src/components/AiActionsDropdown/styles.module.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/AiActionsDropdown/styles.module.css b/src/components/AiActionsDropdown/styles.module.css index 780b74d06..2637de057 100644 --- a/src/components/AiActionsDropdown/styles.module.css +++ b/src/components/AiActionsDropdown/styles.module.css @@ -10,13 +10,18 @@ padding: 6px 14px; border: none; border-radius: 8px; - background: var(--ifm-color-primary); + background: #052B65; color: #fff; font-size: 0.85rem; cursor: pointer; transition: opacity 0.15s, box-shadow 0.15s; } +:global(html[data-theme='dark']) .trigger { + background: #CAE9FF; + color: #052B65; +} + .trigger:hover { opacity: 0.9; box-shadow: var(--ifm-global-shadow-lw); From 38f7e967afc4fd901fe066667ec798bb11776491 Mon Sep 17 00:00:00 2001 From: data-cowwboy Date: Fri, 27 Feb 2026 12:46:24 +0530 Subject: [PATCH 3/5] fix: add response.ok check and remove undocumented ChatGPT param - Check res.ok before copying fetched markdown to prevent copying error page HTML on 404s - Remove undocumented hints=search param from ChatGPT URL --- src/components/AiActionsDropdown/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AiActionsDropdown/index.tsx b/src/components/AiActionsDropdown/index.tsx index d735cb756..b8942a088 100644 --- a/src/components/AiActionsDropdown/index.tsx +++ b/src/components/AiActionsDropdown/index.tsx @@ -63,7 +63,7 @@ export default function AiActionsDropdown({ editUrl }: Props) { const openInChatGPT = () => { const q = encodeURIComponent(`Read from ${pageUrl} so I can ask questions about it`) - window.open(`https://chatgpt.com/?hints=search&q=${q}`, '_blank') + window.open(`https://chatgpt.com/?q=${q}`, '_blank') setOpen(false) } @@ -77,6 +77,7 @@ export default function AiActionsDropdown({ editUrl }: Props) { if (rawUrl) { try { const res = await fetch(rawUrl) + if (!res.ok) throw new Error() const md = await res.text() await navigator.clipboard.writeText(md) setCopied(true) From e68e1a0b93ff91edd61f8f11bf252ab3f391209b Mon Sep 17 00:00:00 2001 From: data-cowwboy Date: Fri, 27 Feb 2026 16:15:06 +0530 Subject: [PATCH 4/5] incorporating feedback --- .../AiActionsDropdown/styles.module.css | 2 +- src/theme/DocItem/Content/index.tsx | 39 +++++++++++++++++++ src/theme/DocItem/Layout/index.tsx | 17 -------- 3 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 src/theme/DocItem/Content/index.tsx delete mode 100644 src/theme/DocItem/Layout/index.tsx diff --git a/src/components/AiActionsDropdown/styles.module.css b/src/components/AiActionsDropdown/styles.module.css index 2637de057..3d4d6d489 100644 --- a/src/components/AiActionsDropdown/styles.module.css +++ b/src/components/AiActionsDropdown/styles.module.css @@ -99,7 +99,7 @@ .itemDesc { font-size: 0.75rem; - color: var(--ifm-color-emphasis-500); + color: var(--ifm-color-emphasis-700); line-height: 1.3; } diff --git a/src/theme/DocItem/Content/index.tsx b/src/theme/DocItem/Content/index.tsx new file mode 100644 index 000000000..93a9f987f --- /dev/null +++ b/src/theme/DocItem/Content/index.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import clsx from 'clsx' +import {ThemeClassNames} from '@docusaurus/theme-common' +import {useDoc} from '@docusaurus/plugin-content-docs/client' +import Heading from '@theme/Heading' +import MDXContent from '@theme/MDXContent' +import AiActionsDropdown from '@site/src/components/AiActionsDropdown' + +function useSyntheticTitle() { + const {metadata, frontMatter, contentTitle} = useDoc() + const shouldRender = + !frontMatter.hide_title && typeof contentTitle === 'undefined' + if (!shouldRender) { + return null + } + return metadata.title +} + +export default function DocItemContent({children}: {children: React.ReactNode}) { + const syntheticTitle = useSyntheticTitle() + const {metadata} = useDoc() + + return ( +
+ {syntheticTitle && ( +
+ {syntheticTitle} + +
+ )} + {!syntheticTitle && ( +
+ +
+ )} + {children} +
+ ) +} diff --git a/src/theme/DocItem/Layout/index.tsx b/src/theme/DocItem/Layout/index.tsx deleted file mode 100644 index 40b93ca57..000000000 --- a/src/theme/DocItem/Layout/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' -import OriginalDocItemLayout from '@theme-original/DocItem/Layout' -import { useDoc } from '@docusaurus/plugin-content-docs/client' -import AiActionsDropdown from '@site/src/components/AiActionsDropdown' - -export default function DocItemLayout(props: React.ComponentProps) { - const { metadata } = useDoc() - - return ( - <> -
- -
- - - ) -} From 12e7cccad5c3b2852ab54058e8667288f76c890e Mon Sep 17 00:00:00 2001 From: data-cowwboy Date: Thu, 5 Mar 2026 18:03:16 +0530 Subject: [PATCH 5/5] fix: add top margin to LLM dropdown on pages with MDX titles Adds spacing between the breadcrumb area and the "Use with LLM" button on pages like /cow-amm where the title comes from MDX content. Co-Authored-By: Claude Opus 4.6 --- src/theme/DocItem/Content/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme/DocItem/Content/index.tsx b/src/theme/DocItem/Content/index.tsx index 93a9f987f..d0f3ffed4 100644 --- a/src/theme/DocItem/Content/index.tsx +++ b/src/theme/DocItem/Content/index.tsx @@ -29,7 +29,7 @@ export default function DocItemContent({children}: {children: React.ReactNode}) )} {!syntheticTitle && ( -
+
)}