feat(theme/llms): add configurable placement option for LLM UI buttons#3163
Conversation
- Add `placement` option to `llmsUI` config ('title' | 'outline')
- Create LlmsCopyRow and LlmsOpenRow components for outline display
- Original 'title' placement shows buttons below H1 (default)
- New 'outline' placement shows as separate rows in outline sidebar
- Refactor title.tsx to conditionally render based on placement
- Update Outline component to render LLM rows when placement='outline'
- Add styles for outline action rows with proper spacing (10px gap)
- Update documentation in config-theme.mdx, ssg-md.mdx, and plugin-llms.mdx (en/zh)
- Update e2e fixtures config for plugin-llms tests
Update API docs, guides, and plugin docs (en/zh) to document the new placement configuration option. Add examples for outline placement.
There was a problem hiding this comment.
Pull request overview
Adds a new themeConfig.llmsUI.placement option to control whether LLMS UI actions render under the page title (default) or in the right-side outline panel, and updates docs accordingly.
Changes:
- Extend
LlmsUItheme config typing/docs withplacement?: 'title' | 'outline'. - Add new outline-row UI components (
LlmsCopyRow,LlmsOpenRow) and supporting outline styles. - Update EN/ZH documentation and adjust the plugin-llms e2e fixture config to use
placement: 'outline'.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| website/docs/zh/plugin/official-plugins/llms.mdx | Documents optional outline placement for LLMS UI. |
| website/docs/zh/guide/basic/ssg-md.mdx | Documents placement: 'outline' behavior for SSG-MD LLMS UI. |
| website/docs/zh/api/config/config-theme.mdx | Adds placement to the LlmsUI config API docs (ZH). |
| website/docs/en/plugin/official-plugins/llms.mdx | Documents optional outline placement for LLMS UI. |
| website/docs/en/guide/basic/ssg-md.mdx | Documents placement: 'outline' behavior for SSG-MD LLMS UI. |
| website/docs/en/api/config/config-theme.mdx | Adds placement to the LlmsUI config API docs (EN). |
| packages/shared/src/types/theme/index.ts | Adds `placement?: 'title' |
| packages/core/src/theme/components/Outline/index.scss | Adds styling hooks for outline “action rows” and dropdown positioning. |
| packages/core/src/theme/components/Outline/LlmsOpenRow.tsx | New outline row dropdown for “Open in …” actions. |
| packages/core/src/theme/components/Outline/LlmsCopyRow.tsx | New outline row action for copying Markdown content. |
| packages/core/src/theme/components/DocContent/docComponents/title.tsx | Hides title-area LLMS UI when placement !== 'title'. |
| e2e/fixtures/plugin-llms/rspress.config.ts | Sets fixture config to llmsUI.placement = 'outline'. |
Comments suppressed due to low confidence (6)
packages/core/src/theme/components/Outline/LlmsOpenRow.tsx:116
LlmsOpenRowrenders a custom dropdown but lacks basic accessibility wiring (e.g.,aria-haspopup,aria-expanded, and keyboard support like closing on Escape / focusing the first item when opened). Since this is a new UI entry point in the outline sidebar, please add the relevant ARIA attributes and minimal keyboard handling to make it usable without a mouse.
<button
className="rp-outline__action-row"
onClick={() => setIsOpen(!isOpen)}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>
<span>{t('openInText', { name: 'chat' })}</span>
</button>
{isOpen && (
<div className="rp-llms-view-options__menu">
{options.map(option => {
packages/core/src/theme/components/Outline/index.scss:80
.rp-outline__action-rowis used by buttons/links (including a disabled loading state inLlmsCopyRow), but there is no:disabledstyling. This means disabled rows still show a pointer cursor and hover color changes. Add a&:disabledrule (e.g., adjust cursor/opacity and suppress hover) to reflect the disabled state.
&__action-row {
cursor: pointer;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 20px;
color: var(--rp-c-text-2);
background: transparent;
display: flex;
align-items: center;
gap: 8px;
border: none;
padding: 0;
text-decoration: none;
transition: color 0.2s;
&:hover {
color: var(--rp-c-text-1);
}
packages/core/src/theme/components/Outline/LlmsCopyRow.tsx:36
LlmsCopyRowduplicates the fetch+cache+clipboard logic fromLlmsCopyButton(separate module-level cache, timer handling, etc.). To avoid divergence (e.g., different success timeout values and future bugfixes needing to be applied twice), consider extracting the shared copy-to-clipboard behavior into a small shared hook/utility used by both components.
const cache = new Map<string, string>();
export function LlmsCopyRow() {
const t = useI18n();
const { pathname } = useMdUrl();
const [isLoading, setLoading] = useState(false);
const [isFinished, setFinished] = useState(false);
const timer = useRef<number | null>(null);
const handleClick = useCallback(async () => {
if (!pathname) return;
setLoading(true);
try {
const content: string =
cache.get(pathname) ?? (await fetch(pathname).then(res => res.text()));
cache.set(pathname, content);
await navigator.clipboard.writeText(content);
} finally {
setLoading(false);
setFinished(true);
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
timer.current = window.setTimeout(() => {
setFinished(false);
timer.current = null;
}, 1500);
}
}, [pathname]);
e2e/fixtures/plugin-llms/rspress.config.ts:13
- This fixture now sets
llmsUI.placement: 'outline', but there is no e2e assertion covering the new placement behavior (e.g., buttons should appear in the outline panel and not under the H1). Please add/update a Playwright test in the plugin-llms fixture to exerciseplacement: 'outline'so regressions are caught.
themeConfig: {
llmsUI: { placement: 'outline' },
},
packages/core/src/theme/components/DocContent/docComponents/title.tsx:27
placement: 'outline'disables rendering of the LLMS UI under the H1, but this PR doesn't add any corresponding render path in the outline sidebar (e.g.,Outlinestill only rendersScrollToTop). As a result, enablingllmsUI.placement = 'outline'currently hides the UI entirely. Please wire the newLlmsCopyRow/LlmsOpenRowinto the outline component (guarded byprocess.env.ENABLE_LLMS_UIandplacement === 'outline').
const { site } = useSite();
const llmsUI = site?.themeConfig?.llmsUI;
const placement =
typeof llmsUI === 'object' ? (llmsUI?.placement ?? 'title') : 'title';
return (
<>
<h1 className={clsx('rp-toc-include', className)} {...rest}>
{children} <Tag tag={tag} />
</h1>
{process.env.ENABLE_LLMS_UI && placement === 'title' && (
<LlmsContainer>
<LlmsCopyButton />
<LlmsViewOptions />
</LlmsContainer>
packages/core/src/theme/components/Outline/LlmsOpenRow.tsx:113
- The trigger label uses
t('openInText', { name: 'chat' }), which will render as “Open in chat” / “在 chat 中打开” and is inconsistent with the menu items (“ChatGPT”, “Claude”). Consider using a more specific i18n key for the trigger (e.g., “Open in chat”) or pass a properly cased/display name (e.g., “Chat”).
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>
<span>{t('openInText', { name: 'chat' })}</span>
</button>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Thanks for the effort here, @Huxpro The LLM UI buttons are intentionally placed near the title (and below the title on mobile / after the breadcrumb), following a common convention across documentation frameworks. Users have likely already developed muscle memory around this location, so adding an alternative placement may introduce confusion rather than convenience.
https://www.fumadocs.dev/docs/guides/rss
https://bun.com/docs/installation https://platform.claude.com/docs/en/intro
https://ui.nuxt.com/docs/getting-started
https://nextjs.org/docs/app/getting-started/project-structure
After looking into this, I think we'll keep the current behavior as-is for now. If you have a specific use case where the current placement falls short, it is more appropriate to configure through custom themes I think 🤔 |
|
https://elements.ai-sdk.dev/ put it on outline and I want it on lynxjs.org:
I also made it configurable and default to "title" so it doesn't change the original behavior, right? It doesn't hurt to have more options |
Get ❤, thanks |










Summary
Add a
placementoption tollmsUIconfig that controls where LLM UI components (copy markdown, open in chat) are displayed:'title'(default): buttons below the H1 title, preserving the original behavior'outline': separate rows in the right-side outline panel, similar to "Scroll to top"New outline components (
LlmsCopyRow,LlmsOpenRow) are added with styling that matches existing outline bottom items. Documentation updated across all relevant pages (en/zh).Related Issue
Checklist