Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
135 changes: 135 additions & 0 deletions site/src/shared/components/AgentPrompt/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 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="Explain the borrow checker in Move ..."
// />

import React, { 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), 1500);
};

const openAgent = (agent: Agent) => {
if (typeof window !== "undefined") {
window.open(agent.url(prompt), "_blank", "noopener");
}
setOpen(false);
};

return (
<aside className={styles.callout}>
<span className={styles.eyebrow}>
Using an agent? Try this prompt
</span>
<pre className={styles.prompt}>{prompt}</pre>
<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}
aria-haspopup="true"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
>
Open in agent
<span className={styles.caret} aria-hidden="true">
</span>
</button>
{open && (
<div className={styles.dropdown} role="menu">
{AGENTS.map((agent) => (
<button
key={agent.id}
type="button"
role="menuitem"
className={styles.item}
onClick={() => openAgent(agent)}
>
{agent.label}
</button>
))}
</div>
)}
</div>
</div>
</aside>
);
}
125 changes: 125 additions & 0 deletions site/src/shared/components/AgentPrompt/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Copyright (c) Mysten Labs, Inc.
* SPDX-License-Identifier: Apache-2.0
*
* Styles for the AgentPrompt callout. Theme-aware through Infima variables.
*/

.callout {
display: block;
margin: 0 0 1.75rem;
padding: 1.1rem 1.25rem;
border: 1px solid var(--ifm-color-emphasis-300);
border-left: 3px solid var(--ifm-color-primary);
border-radius: 8px;
background-color: var(--ifm-background-surface-color);
}

.eyebrow {
display: block;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--ifm-color-primary);
}

.prompt {
margin: 0.7rem 0 0.9rem;
padding: 0.75rem 0.85rem;
font-family: var(--ifm-font-family-monospace);
font-size: 0.82rem;
line-height: 1.55;
color: var(--ifm-color-emphasis-900);
background-color: var(--ifm-code-background);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 6px;
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
}

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

.copyBtn,
.agentBtn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.82rem;
font-weight: 600;
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(--ifm-color-emphasis-300);
background: transparent;
color: var(--ifm-color-emphasis-800);
}

.copyBtn:hover {
border-color: var(--ifm-color-emphasis-500);
color: var(--ifm-heading-color);
}

.agentBtn {
border: 1px solid var(--ifm-color-primary);
background: var(--ifm-color-primary);
color: #ffffff;
}

.agentBtn:hover {
background: var(--ifm-color-primary-dark);
border-color: var(--ifm-color-primary-dark);
}

.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(--ifm-color-emphasis-300);
border-radius: 8px;
background-color: var(--ifm-background-surface-color);
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(--ifm-color-emphasis-800);
cursor: pointer;
}

.item:hover {
background-color: var(--ifm-color-emphasis-200);
color: var(--ifm-heading-color);
}
Loading
Loading