Skip to content

Add sidebar in docs for better integration with LLMs #1231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 25, 2025
179 changes: 179 additions & 0 deletions src/theme/DocActionsDropdown/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import React, { useState } from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';

export default function DocActionsDropdown() {
const [isOpen, setIsOpen] = useState(false);

const buildRawUrl = (path, isIndex) => {
if (isIndex) {
// For index files, use path/index.md
return `https://raw.githubusercontent.com/onflow/docs/main/docs/${path}/index.md`;
} else {
// For regular files, use path.md
return `https://raw.githubusercontent.com/onflow/docs/main/docs/${path}.md`;
}
};

const fetchMarkdown = async (path) => {
// First, try to determine if this is an index.md file by checking both paths
const directPath = `https://raw.githubusercontent.com/onflow/docs/main/docs/${path}.md`;
const indexPath = `https://raw.githubusercontent.com/onflow/docs/main/docs/${path}/index.md`;

try {
// Try the index path first
const indexResponse = await fetch(indexPath);
if (indexResponse.ok) {
return { url: indexPath, text: await indexResponse.text() };
}

// If index path fails, try the direct path
const directResponse = await fetch(directPath);
if (directResponse.ok) {
return { url: directPath, text: await directResponse.text() };
}

// If both fail, return null
return null;
} catch (error) {
console.error('Error fetching markdown:', error);
return null;
}
};

const handleCopyMarkdown = async () => {
try {
const path = window.location.pathname.replace(/^\/docs\/?/, '').replace(/\/$/, '');
const result = await fetchMarkdown(path);

if (result) {
navigator.clipboard.writeText(result.text);
setIsOpen(false);
} else {
throw new Error('Could not fetch markdown');
}
} catch (error) {
console.error('Error copying markdown:', error);
// Fallback to GitHub
const currentPath = window.location.pathname.replace(/^\/docs\/?/, '');
window.open(`https://github.com/onflow/docs/tree/main/docs/${currentPath}`, '_blank');
}
};

const handleViewMarkdown = async () => {
try {
const path = window.location.pathname.replace(/^\/docs\/?/, '').replace(/\/$/, '');
const result = await fetchMarkdown(path);

if (result) {
window.open(result.url, '_blank');
setIsOpen(false);
} else {
// Fallback to GitHub
const currentPath = window.location.pathname.replace(/^\/docs\/?/, '');
window.open(`https://github.com/onflow/docs/tree/main/docs/${currentPath}`, '_blank');
}
} catch (error) {
console.error('Error viewing markdown:', error);
// Fallback to GitHub
const currentPath = window.location.pathname.replace(/^\/docs\/?/, '');
window.open(`https://github.com/onflow/docs/tree/main/docs/${currentPath}`, '_blank');
}
};

const handleOpenInChatGPT = async () => {
try {
const path = window.location.pathname.replace(/^\/docs\/?/, '').replace(/\/$/, '');
const result = await fetchMarkdown(path);

if (result) {
const prompt = `Analyze this documentation: ${result.url}. After reading, ask me what I'd like to know. Keep responses focused on the content.`;
const encodedPrompt = encodeURIComponent(prompt);
window.open(`https://chatgpt.com/?q=${encodedPrompt}`, '_blank');
} else {
// Fallback to current URL
const currentUrl = window.location.href;
const prompt = `Analyze this documentation: ${currentUrl}. After reading, ask me what I'd like to know. Keep responses focused on the content.`;
const encodedPrompt = encodeURIComponent(prompt);
window.open(`https://chatgpt.com/?q=${encodedPrompt}`, '_blank');
}
setIsOpen(false);
} catch (error) {
console.error('Error opening in ChatGPT:', error);
// Fallback to current URL
const currentUrl = window.location.href;
const prompt = `Analyze this documentation: ${currentUrl}. After reading, ask me what I'd like to know. Keep responses focused on the content.`;
const encodedPrompt = encodeURIComponent(prompt);
window.open(`https://chatgpt.com/?q=${encodedPrompt}`, '_blank');
setIsOpen(false);
}
};

const handleOpenInClaude = async () => {
try {
const path = window.location.pathname.replace(/^\/docs\/?/, '').replace(/\/$/, '');
const result = await fetchMarkdown(path);

if (result) {
const prompt = `Review this documentation: ${result.url}. Once complete, ask me what questions I have. Stay focused on the provided content.`;
const encodedPrompt = encodeURIComponent(prompt);
window.open(`https://claude.ai/chat/new?prompt=${encodedPrompt}`, '_blank');
} else {
// Fallback to current URL
const currentUrl = window.location.href;
const prompt = `Review this documentation: ${currentUrl}. Once complete, ask me what questions I have. Stay focused on the provided content.`;
const encodedPrompt = encodeURIComponent(prompt);
window.open(`https://claude.ai/chat/new?prompt=${encodedPrompt}`, '_blank');
}
setIsOpen(false);
} catch (error) {
console.error('Error opening in Claude:', error);
// Fallback to current URL
const currentUrl = window.location.href;
const prompt = `Review this documentation: ${currentUrl}. Once complete, ask me what questions I have. Stay focused on the provided content.`;
const encodedPrompt = encodeURIComponent(prompt);
window.open(`https://claude.ai/chat/new?prompt=${encodedPrompt}`, '_blank');
setIsOpen(false);
}
};

const handleOpenFlowKnowledge = () => {
window.open('https://github.com/onflow/Flow-Data-Sources/tree/main/merged_docs', '_blank');
setIsOpen(false);
};

const handleArrowClick = (e) => {
e.stopPropagation();
setIsOpen(!isOpen);
};

return (
<div className={styles.dropdownContainer}>
<button
className={styles.dropdownButton}
onClick={handleOpenInChatGPT}
>
Open in ChatGPT
<span className={styles.arrow} onClick={handleArrowClick} />
</button>
{isOpen && (
<div className={styles.dropdownMenu}>
<button onClick={handleOpenInClaude} className={styles.menuItem}>
Open in Claude
</button>
<div className={styles.divider} />
<button onClick={handleCopyMarkdown} className={styles.menuItem}>
Copy as Markdown
</button>
<button onClick={handleViewMarkdown} className={styles.menuItem}>
View Source Markdown
</button>
<div className={styles.divider} />
<button onClick={handleOpenFlowKnowledge} className={styles.menuItem}>
Full Flow Knowledge Source
</button>
</div>
)}
</div>
);
}
112 changes: 112 additions & 0 deletions src/theme/DocActionsDropdown/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
.dropdownContainer {
position: relative;
display: inline-block;
width: 100%;
margin-top: 1rem;
}

.dropdownButton {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 8px 16px;
background-color: var(--ifm-color-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
position: relative;
}

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

.arrow {
width: 40%;
height: 100%;
position: absolute;
right: 0;
top: 0;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8px;
cursor: pointer;
}

.arrow::after {
content: '';
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid white;
transition: transform 0.2s ease;
}

.dropdownButton:hover .arrow::after {
transform: translateY(2px);
}

.dropdownButton[aria-expanded="true"] .arrow {
transform: rotate(180deg);
}

.dropdownMenu {
position: fixed;
width: calc(100% - 30px);
max-width: 300px;
background: var(--ifm-background-color);
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 9999;
margin-top: 0.5rem;
max-height: 80vh;
overflow-y: auto;
}

.menuItem {
display: block;
width: 100%;
padding: 0.75rem 1rem;
text-align: left;
border: none;
background: none;
color: var(--ifm-font-color-base);
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s ease;
}

.menuItem:hover {
background: var(--ifm-color-emphasis-100);
color: var(--ifm-font-color-base);
}

.divider {
height: 1px;
background: var(--ifm-color-emphasis-200);
margin: 0.5rem 0;
}

[data-theme="dark"] .dropdownMenu {
background: var(--ifm-background-color);
border-color: var(--ifm-color-emphasis-300);
}

[data-theme="dark"] .menuItem {
color: var(--ifm-font-color-base);
}

[data-theme="dark"] .menuItem:hover {
background: var(--ifm-color-emphasis-200);
color: var(--ifm-font-color-base);
}

[data-theme="dark"] .divider {
background: var(--ifm-color-emphasis-300);
}
13 changes: 13 additions & 0 deletions src/theme/DocItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import DocItem from '@theme-original/DocItem';
import type { Props } from '@theme/DocItem';

export default function DocItemWrapper(props: Props): JSX.Element {
return (
<div className="doc-item-wrapper">
<div className="doc-actions-container">
</div>
<DocItem {...props} />
</div>
);
}
19 changes: 19 additions & 0 deletions src/theme/DocItem/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.doc-item-wrapper {
position: relative;
}

.doc-actions-container {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 10;
}

@media (max-width: 996px) {
.doc-actions-container {
position: static;
margin-bottom: 1rem;
display: flex;
justify-content: flex-end;
}
}
19 changes: 19 additions & 0 deletions src/theme/Layout/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.layoutWrapper {
position: relative;
}

.docActionsContainer {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
}

@media (max-width: 996px) {
.docActionsContainer {
position: static;
margin: 1rem;
display: flex;
justify-content: flex-end;
}
}
4 changes: 4 additions & 0 deletions src/theme/TOC/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import clsx from 'clsx';
import TOCItems from '@theme/TOCItems';
import type { Props } from '@theme/TOC';
import DocActionsDropdown from '../DocActionsDropdown';

import styles from './styles.module.css';
import FeedbackFaces from '@site/src/components/feedbackFaces';
Expand All @@ -17,6 +18,9 @@ export default function TOC({ className, ...props }: Props): JSX.Element {
<div className="p-1">
<h6 className="mb-0 p-1">Rate this page</h6>
<FeedbackFaces />
<div className={styles.docActionsContainer}>
<DocActionsDropdown />
</div>
</div>
<TOCItems
{...props}
Expand Down