Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/docs/app/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import defaultMdxComponents from 'fumadocs-ui/mdx';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';
import { notFound } from 'next/navigation';

import AgentPrompt from '@/components/AgentPrompt';
import { source } from '@/lib/source';

export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
Expand Down Expand Up @@ -33,6 +34,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }>
components={{
...defaultMdxComponents,
TypeTable,
AgentPrompt,
}}
/>
</DocsBody>
Expand Down
112 changes: 112 additions & 0 deletions packages/docs/components/AgentPrompt/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//
// AgentPrompt — a callout that surfaces a ready-made builder prompt at the top
// of a docs page. Readers can copy the prompt or open it directly in an AI
// agent (Claude, ChatGPT, or Gemini) with the prompt pre-filled.
//
// Usage in MDX (registered globally, no import needed):
//
// <AgentPrompt
// prompt="Set up this machine for Sui development: ..."
// />

'use client';

import { useEffect, useRef, useState } from 'react';
import styles from './styles.module.css';

interface Agent {
id: string;
label: string;
url: (prompt: string) => string;
}

const AGENTS: Agent[] = [
{
id: 'claude',
label: 'Claude',
url: (p) => `https://claude.ai/new?q=${encodeURIComponent(p)}`,
},
{
id: 'chatgpt',
label: 'ChatGPT',
url: (p) => `https://chatgpt.com/?q=${encodeURIComponent(p)}`,
},
{
id: 'gemini',
label: 'Gemini',
url: (p) => `https://gemini.google.com/app?q=${encodeURIComponent(p)}`,
},
];

export default function AgentPrompt({ prompt }: { prompt: string }) {
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
const [pageName, setPageName] = useState('');

useEffect(() => {
setPageName(window.location.pathname.replace(/\//g, '+').replace(/^\+/, ''));
}, []);

useEffect(() => {
if (!open) return;
const onClick = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('mousedown', onClick);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onClick);
document.removeEventListener('keydown', onKey);
};
}, [open]);

const copyPrompt = () => {
if (typeof navigator !== 'undefined' && navigator.clipboard) {
navigator.clipboard.writeText(prompt);
}
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};

return (
<div className={styles.root}>
<div className={styles.label}>Agent prompt</div>
<p className={styles.text}>{prompt}</p>
<div className={styles.actions}>
<button type="button" className={styles.copyBtn} onClick={copyPrompt}>
{copied ? '✓ Copied' : 'Copy prompt'}
</button>
<div className={styles.agentWrap} ref={menuRef}>
<button type="button" className={styles.agentBtn} onClick={() => setOpen(!open)}>
Open in agent <span className={styles.caret}>▾</span>
</button>
{open && (
<div className={styles.dropdown}>
{AGENTS.map((agent) => (
<a
key={agent.id}
href={agent.url(prompt)}
target="_blank"
rel="noopener noreferrer"
className={styles.item}
onClick={() => setOpen(false)}
>
{agent.label}
</a>
))}
</div>
)}
</div>
</div>
</div>
);
}
107 changes: 107 additions & 0 deletions packages/docs/components/AgentPrompt/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* Copyright (c) Mysten Labs, Inc. */
/* SPDX-License-Identifier: Apache-2.0 */

.root {
margin: 1.5rem 0;
padding: 1rem 1.25rem;
border: 1px solid var(--fd-border, hsl(var(--border)));
border-radius: 0.75rem;
background: var(--fd-card, hsl(var(--card)));
}

.label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fd-muted-foreground, hsl(var(--muted-foreground)));
margin-bottom: 0.4rem;
}

.text {
font-size: 0.9rem;
line-height: 1.55;
color: var(--fd-foreground, hsl(var(--foreground)));
margin: 0 0 0.85rem;
}

.actions {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}

.copyBtn,
.agentBtn {
font-size: 0.8rem;
font-family: inherit;
padding: 0.4rem 0.85rem;
border-radius: 6px;
cursor: pointer;
transition:
background-color 0.15s ease,
border-color 0.15s ease,
color 0.15s ease;
}

.copyBtn {
border: 1px solid var(--fd-border, hsl(var(--border)));
background: transparent;
color: var(--fd-foreground, hsl(var(--foreground)));
}

.copyBtn:hover {
border-color: var(--fd-muted-foreground, hsl(var(--muted-foreground)));
}

.agentBtn {
border: 1px solid var(--fd-primary, hsl(var(--primary)));
background: var(--fd-primary, hsl(var(--primary)));
color: var(--fd-primary-foreground, hsl(var(--primary-foreground)));
}

.agentBtn:hover {
opacity: 0.9;
}

.caret {
font-size: 0.7rem;
line-height: 1;
}

.agentWrap {
position: relative;
}

.dropdown {
position: absolute;
top: calc(100% + 0.35rem);
left: 0;
z-index: 20;
min-width: 9rem;
padding: 0.3rem;
border: 1px solid var(--fd-border, hsl(var(--border)));
border-radius: 8px;
background-color: var(--fd-popover, hsl(var(--popover)));
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
}

.item {
display: block;
width: 100%;
text-align: left;
font-size: 0.85rem;
font-family: inherit;
padding: 0.45rem 0.6rem;
border: none;
border-radius: 5px;
background: transparent;
color: var(--fd-foreground, hsl(var(--foreground)));
cursor: pointer;
text-decoration: none;
}

.item:hover {
background-color: var(--fd-accent, hsl(var(--accent)));
}
Loading