From 88456380903a06ab0e71fce870e1d983a50679ef Mon Sep 17 00:00:00 2001 From: Kim Pohas Date: Fri, 5 Jun 2026 03:26:42 -0700 Subject: [PATCH 1/2] DOCS-1685 - Add AI Tools sidebar widget Adds an "AI Tools" widget above the table of contents on every doc page, modeled on Supabase's implementation. Three actions: - Copy as Markdown: fetches raw source from /llm/docs/ mirror, falls back to DOM extraction for dev/local use - Ask Claude: deep-links to claude.ai/new with the page URL pre-filled - Ask ChatGPT: deep-links to chatgpt.com with the page URL pre-filled Implemented via Docusaurus theme swizzle (DocItem/TOC/Desktop wrapper). Co-Authored-By: Claude Sonnet 4.6 --- src/components/AITools/index.tsx | 69 ++++++++++++++++++++++++ src/components/AITools/styles.module.css | 41 ++++++++++++++ src/theme/DocItem/TOC/Desktop/index.tsx | 16 ++++++ 3 files changed, 126 insertions(+) create mode 100644 src/components/AITools/index.tsx create mode 100644 src/components/AITools/styles.module.css create mode 100644 src/theme/DocItem/TOC/Desktop/index.tsx diff --git a/src/components/AITools/index.tsx b/src/components/AITools/index.tsx new file mode 100644 index 0000000000..d7a50d679e --- /dev/null +++ b/src/components/AITools/index.tsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import styles from './styles.module.css'; + +// Derive the mirror URL for the current page. +// /help/docs/foo/bar → /help/llm/docs/foo/bar.md +function getMirrorUrl(): string | null { + if (typeof window === 'undefined') return null; + const match = window.location.pathname.match(/^(\/help\/docs\/.+?)\/?$/); + if (!match) return null; + return `${window.location.origin}${match[1]}.md`; +} + +async function getMarkdownContent(): Promise { + const mirrorUrl = getMirrorUrl(); + if (mirrorUrl) { + try { + const res = await fetch(mirrorUrl); + if (res.ok) return await res.text(); + } catch { /* fall through */ } + } + // DOM fallback + const el = document.querySelector('article') || document.body; + return (el as HTMLElement).innerText.replace(/\s+/g, ' ').trim().slice(0, 4000); +} + +function getPrompt(): string { + const url = typeof window !== 'undefined' ? window.location.href : ''; + return encodeURIComponent(`Read from ${url} so I can ask questions about its contents`); +} + +function CopyIcon() { + return ( + + + + + ); +} + +export default function AITools(): JSX.Element { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + const text = await getMarkdownContent(); + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { /* clipboard not available */ } + }; + + return ( +
+
AI Tools
+ + + + Ask Claude + + + + Ask ChatGPT + +
+ ); +} diff --git a/src/components/AITools/styles.module.css b/src/components/AITools/styles.module.css new file mode 100644 index 0000000000..1e121f6435 --- /dev/null +++ b/src/components/AITools/styles.module.css @@ -0,0 +1,41 @@ +.aiTools { + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 8px; + padding: 12px 14px; + margin-bottom: 16px; +} + +.header { + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--ifm-color-emphasis-600); + margin-bottom: 8px; +} + +.action { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + width: 100%; + background: none; + border: none; + cursor: pointer; + font-size: 0.8rem; + color: var(--ifm-color-emphasis-800); + text-decoration: none; + text-align: left; + line-height: 1.4; +} + +.action:hover { + color: var(--ifm-color-primary); + text-decoration: none; +} + +.action:disabled { + opacity: 0.6; + cursor: default; +} diff --git a/src/theme/DocItem/TOC/Desktop/index.tsx b/src/theme/DocItem/TOC/Desktop/index.tsx new file mode 100644 index 0000000000..70cd690113 --- /dev/null +++ b/src/theme/DocItem/TOC/Desktop/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import TOCDesktop from '@theme-original/DocItem/TOC/Desktop'; +import type TOCDesktopType from '@theme/DocItem/TOC/Desktop'; +import type { WrapperProps } from '@docusaurus/types'; +import AITools from '@site/src/components/AITools'; + +type Props = WrapperProps; + +export default function TOCDesktopWrapper(props: Props): JSX.Element { + return ( + <> + + + + ); +} From e319c06c551603e5cc33ce51280fe98f95d7d37b Mon Sep 17 00:00:00 2001 From: Kim Pohas Date: Wed, 24 Jun 2026 18:03:34 -0700 Subject: [PATCH 2/2] edits --- src/components/AITools/index.tsx | 41 +++++++++++++++++------- src/components/AITools/styles.module.css | 14 +++++--- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/components/AITools/index.tsx b/src/components/AITools/index.tsx index d7a50d679e..c83d4e9f71 100644 --- a/src/components/AITools/index.tsx +++ b/src/components/AITools/index.tsx @@ -5,12 +5,12 @@ import styles from './styles.module.css'; // /help/docs/foo/bar → /help/llm/docs/foo/bar.md function getMirrorUrl(): string | null { if (typeof window === 'undefined') return null; - const match = window.location.pathname.match(/^(\/help\/docs\/.+?)\/?$/); + const match = window.location.pathname.match(/^(.*\/docs\/.+?)\/?$/); if (!match) return null; - return `${window.location.origin}${match[1]}.md`; + return `${window.location.origin}${match[1].replace('/docs/', '/llm/docs/')}.md`; } -async function getMarkdownContent(): Promise { +async function getPageContent(): Promise { const mirrorUrl = getMirrorUrl(); if (mirrorUrl) { try { @@ -18,31 +18,50 @@ async function getMarkdownContent(): Promise { if (res.ok) return await res.text(); } catch { /* fall through */ } } - // DOM fallback + + // Fall back to readable page text when the markdown mirror is unavailable. const el = document.querySelector('article') || document.body; return (el as HTMLElement).innerText.replace(/\s+/g, ' ').trim().slice(0, 4000); } function getPrompt(): string { const url = typeof window !== 'undefined' ? window.location.href : ''; - return encodeURIComponent(`Read from ${url} so I can ask questions about its contents`); + const mirrorUrl = getMirrorUrl(); + const source = mirrorUrl ? `the markdown mirror at ${mirrorUrl} and the source page at ${url}` : `the source page at ${url}`; + return encodeURIComponent(`Read ${source} so I can ask questions about its contents. If you cannot access the page, ask me to paste the copied page content.`); } function CopyIcon() { return ( - + ); } +function ClaudeIcon() { + return ( + + ); +} + +function ChatGPTIcon() { + return ( + + ); +} + export default function AITools(): JSX.Element { const [copied, setCopied] = useState(false); const handleCopy = async () => { try { - const text = await getMarkdownContent(); + const text = await getPageContent(); await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -51,17 +70,17 @@ export default function AITools(): JSX.Element { return (
-
AI Tools
+
Use with AI
- + Ask Claude - + Ask ChatGPT
diff --git a/src/components/AITools/styles.module.css b/src/components/AITools/styles.module.css index 1e121f6435..3a726ba056 100644 --- a/src/components/AITools/styles.module.css +++ b/src/components/AITools/styles.module.css @@ -1,17 +1,17 @@ .aiTools { border: 1px solid var(--ifm-color-emphasis-300); border-radius: 8px; - padding: 12px 14px; + padding: 10px 12px; margin-bottom: 16px; } .header { - font-size: 0.7rem; + font-size: 0.65rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ifm-color-emphasis-600); - margin-bottom: 8px; + margin-bottom: 6px; } .action { @@ -23,7 +23,7 @@ background: none; border: none; cursor: pointer; - font-size: 0.8rem; + font-size: 0.75rem; color: var(--ifm-color-emphasis-800); text-decoration: none; text-align: left; @@ -39,3 +39,9 @@ opacity: 0.6; cursor: default; } + +.icon { + flex: 0 0 14px; + height: 14px; + width: 14px; +}