Skip to content

Commit 4955433

Browse files
Oxyjunmvvmm
andauthored
feat: add Agents toolkit to sidebar with agent resources nav section (#30237)
* [Docs Lab] Add Agents toolkit section to right sidebar Add a new 'Agents toolkit' section in the RHS sidebar that surfaces AI/agent-related actions (setup your agent, copy page link, copy/view as Markdown, open in Claude, open in ChatGPT) directly alongside the table of contents, above the existing feedback prompt. * [Docs Lab] Portal-based tooltips and UI refinements for Agents toolkit - Replace native title tooltips with portal-based styled tooltip popups - Convert View as Markdown, Open in Claude, Open in ChatGPT to icon buttons - Remove Copy page link option - Reorder sections: Agents toolkit now sits above Was this helpful - Add horizontal divider between Agents toolkit and feedback prompt - Update tooltip text for icon buttons * fix: format AgentsToolkit with Prettier * fix: update Agents toolkit nav links, icons, and remove CopyPageButton * fix: format AgentsToolkit and sidebar with Prettier --------- Co-authored-by: mvm <vance@cloudflare.com>
1 parent b3cb75f commit 4955433

6 files changed

Lines changed: 280 additions & 242 deletions

File tree

src/components/AgentsToolkit.tsx

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import {
2+
PiCopyDuotone,
3+
PiArrowSquareOutLight,
4+
PiSparkleDuotone,
5+
PiCheckCircleLight,
6+
} from "react-icons/pi";
7+
import { useState, useRef, useCallback } from "react";
8+
import { createPortal } from "react-dom";
9+
import ClaudeIcon from "./icons/ClaudeIcon";
10+
import ChatGPTIcon from "./icons/ChatGPTIcon";
11+
import { track } from "~/util/zaraz";
12+
13+
type CopyFeedback = { key: string; state: "success" } | null;
14+
15+
interface ListOption {
16+
key: string;
17+
label: string;
18+
tooltip: string;
19+
icon: React.ComponentType<{ className?: string }>;
20+
onClick: () => void;
21+
}
22+
23+
interface IconOption {
24+
key: string;
25+
tooltip: string;
26+
icon: React.ComponentType<{ className?: string }>;
27+
onClick: () => void;
28+
}
29+
30+
function useTooltip() {
31+
const [visible, setVisible] = useState(false);
32+
const [position, setPosition] = useState({ top: 0, left: 0 });
33+
const triggerRef = useRef<HTMLElement | null>(null);
34+
35+
const show = useCallback(() => {
36+
if (triggerRef.current) {
37+
const rect = triggerRef.current.getBoundingClientRect();
38+
setPosition({
39+
top: rect.top - 8,
40+
left: rect.left + rect.width / 2,
41+
});
42+
}
43+
setVisible(true);
44+
}, []);
45+
46+
const hide = useCallback(() => {
47+
setVisible(false);
48+
}, []);
49+
50+
return { visible, position, triggerRef, show, hide };
51+
}
52+
53+
function TooltipPortal({
54+
text,
55+
visible,
56+
position,
57+
}: {
58+
text: string;
59+
visible: boolean;
60+
position: { top: number; left: number };
61+
}) {
62+
if (!visible) return null;
63+
64+
return createPortal(
65+
<div
66+
role="tooltip"
67+
className="pointer-events-none fixed z-[99999] -translate-x-1/2 -translate-y-full rounded-md bg-[var(--sl-color-bg-nav)] px-3 py-2 text-center text-[12px] leading-snug text-[var(--sl-color-text)] shadow-lg ring-1 ring-[var(--sl-color-hairline)]"
68+
style={{ top: position.top, left: position.left }}
69+
>
70+
{text}
71+
</div>,
72+
document.body,
73+
);
74+
}
75+
76+
function ListItem({
77+
tooltip,
78+
onClick,
79+
justCopied,
80+
icon: Icon,
81+
label,
82+
}: {
83+
tooltip: string;
84+
onClick: () => void;
85+
justCopied: boolean;
86+
icon: React.ComponentType<{ className?: string }>;
87+
label: string;
88+
}) {
89+
const { visible, position, triggerRef, show, hide } = useTooltip();
90+
91+
return (
92+
<li className="m-0 p-0">
93+
<TooltipPortal text={tooltip} visible={visible} position={position} />
94+
<button
95+
ref={triggerRef as React.RefObject<HTMLButtonElement>}
96+
onMouseEnter={show}
97+
onMouseLeave={hide}
98+
onFocus={show}
99+
onBlur={hide}
100+
onClick={onClick}
101+
className="flex w-full cursor-pointer items-center gap-2.5 rounded-sm border-0 bg-transparent px-0 py-1 text-[13px] text-[var(--sl-color-gray-2)] shadow-none transition-colors duration-150 ease-out hover:text-[var(--sl-color-white)] focus-visible:ring-2 focus-visible:ring-[var(--sl-color-text-accent)] focus-visible:outline-none"
102+
>
103+
{justCopied ? (
104+
<PiCheckCircleLight className="h-3.5 w-3.5 shrink-0 text-green-500" />
105+
) : (
106+
<Icon className="h-3.5 w-3.5 shrink-0" />
107+
)}
108+
<span>{justCopied ? "Copied!" : label}</span>
109+
</button>
110+
</li>
111+
);
112+
}
113+
114+
function IconButton({
115+
tooltip,
116+
onClick,
117+
icon: Icon,
118+
}: {
119+
tooltip: string;
120+
onClick: () => void;
121+
icon: React.ComponentType<{ className?: string }>;
122+
}) {
123+
const { visible, position, triggerRef, show, hide } = useTooltip();
124+
125+
return (
126+
<div>
127+
<TooltipPortal text={tooltip} visible={visible} position={position} />
128+
<button
129+
ref={triggerRef as React.RefObject<HTMLButtonElement>}
130+
onMouseEnter={show}
131+
onMouseLeave={hide}
132+
onFocus={show}
133+
onBlur={hide}
134+
onClick={onClick}
135+
className="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-[var(--sl-color-gray-2)] shadow-none transition-colors duration-150 ease-out hover:bg-[var(--color-cl1-gray-9)] hover:text-[var(--sl-color-white)] focus-visible:ring-2 focus-visible:ring-[var(--sl-color-text-accent)] focus-visible:outline-none dark:hover:bg-[var(--color-cl1-gray-2)]"
136+
>
137+
<Icon className="h-4 w-4" />
138+
<span className="sr-only">{tooltip}</span>
139+
</button>
140+
</div>
141+
);
142+
}
143+
144+
export default function AgentsToolkit() {
145+
const [copyFeedback, setCopyFeedback] = useState<CopyFeedback>(null);
146+
147+
const showFeedback = (key: string) => {
148+
setCopyFeedback({ key, state: "success" });
149+
setTimeout(() => setCopyFeedback(null), 1500);
150+
};
151+
152+
const handleCopyMarkdown = async () => {
153+
const markdownUrl = new URL("index.md", window.location.href).toString();
154+
try {
155+
const clipboardItem = new ClipboardItem({
156+
["text/plain"]: fetch(markdownUrl)
157+
.then((r) => r.text())
158+
.then((t) => new Blob([t], { type: "text/plain" }))
159+
.catch((e) => {
160+
throw new Error(`Received ${e.message} for ${markdownUrl}`);
161+
}),
162+
});
163+
await navigator.clipboard.write([clipboardItem]);
164+
track("agents toolkit clicked", { value: "copy markdown" });
165+
showFeedback("copy-md");
166+
} catch (error) {
167+
console.error("Failed to copy Markdown:", error);
168+
}
169+
};
170+
171+
const handleViewMarkdown = () => {
172+
const markdownUrl = new URL("index.md", window.location.href).toString();
173+
track("agents toolkit clicked", { value: "view markdown" });
174+
window.open(markdownUrl, "_blank");
175+
};
176+
177+
const handleExternalAI = (url: string, vendor: string) => {
178+
const indexMdUrl = new URL("index.md", window.location.href).toString();
179+
const prompt = `Read this page from the Cloudflare docs: ${encodeURIComponent(indexMdUrl)} and answer questions about the content.`;
180+
track("agents toolkit clicked", { value: `${vendor} ai` });
181+
window.open(`${url}${prompt}`, "_blank");
182+
};
183+
184+
const handleViewAIOptions = () => {
185+
track("agents toolkit clicked", { value: "view ai options" });
186+
window.open("/agent-setup/", "_blank");
187+
};
188+
189+
const listOptions: ListOption[] = [
190+
{
191+
key: "ai-options",
192+
label: "Agent setup",
193+
tooltip:
194+
"Setup your agent with the necessary tools to build on Cloudflare",
195+
icon: PiSparkleDuotone,
196+
onClick: handleViewAIOptions,
197+
},
198+
{
199+
key: "copy-md",
200+
label: "Copy as Markdown",
201+
tooltip: "Copy this page's Markdown source to clipboard",
202+
icon: PiCopyDuotone,
203+
onClick: handleCopyMarkdown,
204+
},
205+
];
206+
207+
const iconOptions: IconOption[] = [
208+
{
209+
key: "view-md",
210+
tooltip: "Open the Markdown file in a new tab",
211+
icon: PiArrowSquareOutLight,
212+
onClick: handleViewMarkdown,
213+
},
214+
{
215+
key: "claude",
216+
tooltip: "Ask Claude about this page",
217+
icon: ClaudeIcon,
218+
onClick: () => handleExternalAI("https://claude.ai/new?q=", "claude"),
219+
},
220+
{
221+
key: "chatgpt",
222+
tooltip: "Ask ChatGPT about this page",
223+
icon: ChatGPTIcon,
224+
onClick: () =>
225+
handleExternalAI("https://chat.openai.com/?prompt=", "chatgpt"),
226+
},
227+
];
228+
229+
return (
230+
<div>
231+
<h3 className="mt-0 mb-3 text-[11px] font-semibold tracking-widest text-[var(--sl-color-text-accent)] uppercase">
232+
Agents toolkit
233+
</h3>
234+
<ul className="m-0 flex list-none flex-col gap-0 p-0">
235+
{listOptions.map(({ key, label, tooltip, icon: Icon, onClick }) => {
236+
const justCopied =
237+
copyFeedback?.key === key && copyFeedback.state === "success";
238+
239+
return (
240+
<ListItem
241+
key={key}
242+
tooltip={tooltip}
243+
onClick={onClick}
244+
justCopied={justCopied}
245+
icon={Icon}
246+
label={label}
247+
/>
248+
);
249+
})}
250+
</ul>
251+
252+
<div className="mt-2 flex items-center gap-1">
253+
{iconOptions.map(({ key, tooltip, icon: Icon, onClick }) => (
254+
<IconButton
255+
key={key}
256+
tooltip={tooltip}
257+
onClick={onClick}
258+
icon={Icon}
259+
/>
260+
))}
261+
</div>
262+
</div>
263+
);
264+
}

0 commit comments

Comments
 (0)