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
7 changes: 6 additions & 1 deletion agent-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,10 @@
"langchain": "^0.3.28",
"vitest": "^3.2.3"
},
"overrides": {
"gray-matter": {
"js-yaml": "^3.14.1"
}
},
"module": "index.ts"
}
}
14 changes: 8 additions & 6 deletions app/(docs)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { source } from "@/lib/source";
import { DocsLayout } from "fumadocs-ui/layouts/notebook";
import { LargeSearchToggle } from 'fumadocs-ui/components/layout/search-toggle';
import AISearchToggle from "../../components/AISearchToggle";
import CopyPageDropdown from "../../components/CopyPageDropdown";
import type { ReactNode } from "react";

export default function Layout({ children }: { children: ReactNode }) {
Expand All @@ -12,12 +13,13 @@ export default function Layout({ children }: { children: ReactNode }) {
tree={source.pageTree}
searchToggle={{
components: {
lg: (
<div className="flex gap-1.5 max-md:hidden">
<LargeSearchToggle className="flex-1" />
<AISearchToggle />
</div>
),
lg: (
<div className="flex gap-1.5 max-md:hidden">
<LargeSearchToggle className="flex-1" />
<AISearchToggle />
<CopyPageDropdown />
</div>
),
},
}}
>
Expand Down
47 changes: 47 additions & 0 deletions app/api/page-content/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextRequest } from 'next/server';
import { readFile } from 'fs/promises';
import { join } from 'path';
import matter from 'gray-matter';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const path = searchParams.get('path');

if (!path) {
return new Response('Path parameter required', { status: 400 });
}

if (path.includes('..') || path.includes('\\') || path.startsWith('/')) {
return new Response('Invalid path parameter', { status: 400 });
}

const basePath = join(process.cwd(), 'content');
const indexPath = join(basePath, path, 'index.mdx');
const directPath = join(basePath, `${path}.mdx`);

let fileContent: string;

try {
fileContent = await readFile(indexPath, 'utf-8');
} catch {
try {
fileContent = await readFile(directPath, 'utf-8');
} catch {
return new Response('Page not found', { status: 404 });
}
}

const { content, data } = matter(fileContent);

return Response.json({
content,
title: data.title || '',
description: data.description || '',
path
});
} catch (error) {
console.error('Error reading page content:', error);
return new Response('Failed to read page content', { status: 500 });
}
}
7 changes: 5 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import "./global.css";

// Validate environment variables at startup (server-side only)
if (typeof window === 'undefined') {
validateEnv();
const isValid = validateEnv();
if (!isValid) {
console.warn('Environment validation failed during build – this is expected at build time');
}
}

export const metadata: Metadata = {
Expand Down Expand Up @@ -88,7 +91,7 @@ export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={GeistSans.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen antialiased">
<RootProvider
<RootProvider
theme={{ enabled: true, enableSystem: true }}
>
{children}
Expand Down
139 changes: 139 additions & 0 deletions components/CopyPageDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use client';

import React, { useState } from 'react';
import { usePathname } from 'next/navigation';
import { Copy, ExternalLink, FileText, MessageSquare } from 'lucide-react';
import * as Popover from '@radix-ui/react-popover';

interface PageContent {
content: string;
title: string;
description: string;
path: string;
}

export default function CopyPageDropdown() {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const pathname = usePathname();

const formatMarkdownForLLM = (content: PageContent): string => {
return `# ${content.title}\n\n${content.description ? `${content.description}\n\n` : ''}${content.content}`;
};

const fetchPageContent = async (): Promise<PageContent | null> => {
try {
setIsLoading(true);
const contentPath = pathname.startsWith('/') ? pathname.slice(1) : pathname;

const response = await fetch(`/api/page-content?path=${encodeURIComponent(contentPath)}`);
if (!response.ok) throw new Error('Failed to fetch content');

return await response.json();
} catch (error) {
console.error('Error fetching page content:', error);
return null;
} finally {
setIsLoading(false);
}
Comment on lines +33 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve user feedback for errors.

Currently, errors are only logged to the console. Users don't receive any feedback when operations fail, which creates a poor user experience.

Consider adding user-visible error states or toast notifications to inform users when operations fail.

🤖 Prompt for AI Agents
In components/CopyPageDropdown.tsx around lines 29 to 34, the catch block only
logs errors to the console without informing users. Enhance user experience by
adding a user-visible error notification, such as setting an error state to
display a message in the UI or triggering a toast notification, so users are
aware when the fetch operation fails.

};

const handleCopyMarkdown = async () => {
const content = await fetchPageContent();
if (!content) return;

const markdownForLLM = formatMarkdownForLLM(content);

try {
await navigator.clipboard.writeText(markdownForLLM);
} catch (error) {
console.error('Failed to copy to clipboard:', error);
}
setIsOpen(false);
};

const handleViewMarkdown = async () => {
const content = await fetchPageContent();
if (!content) return;

const markdownForLLM = formatMarkdownForLLM(content);
const blob = new Blob([markdownForLLM], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
setTimeout(() => URL.revokeObjectURL(url), 100);
setIsOpen(false);
};

const handleOpenInChatGPT = async () => {
const content = await fetchPageContent();
if (!content) return;

const markdownForLLM = formatMarkdownForLLM(content);
const chatGPTUrl = `https://chatgpt.com/?q=${encodeURIComponent(`Please help me understand this documentation:\n\n${markdownForLLM}`)}`;
window.open(chatGPTUrl, '_blank');
setIsOpen(false);
};

const handleOpenInClaude = async () => {
const content = await fetchPageContent();
if (!content) return;

const markdownForLLM = formatMarkdownForLLM(content);
const claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(`Please help me understand this documentation:\n\n${markdownForLLM}`)}`;
window.open(claudeUrl, '_blank');
setIsOpen(false);
};

return (
<Popover.Root open={isOpen} onOpenChange={setIsOpen}>
<Popover.Trigger asChild>
<button
aria-label="Copy page options"
className="flex items-center justify-center transition-all duration-200 hover:scale-110 active:scale-95 transform-origin-center border border-gray-200 dark:border-cyan-900 rounded-md size-10 hover:border-cyan-300 dark:hover:border-cyan-600"
>
<Copy className="size-4 text-cyan-700 dark:text-cyan-500" />
</button>
</Popover.Trigger>
<Popover.Content
className="w-64 p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-50"
align="end"
sideOffset={8}
>
<div className="flex flex-col gap-1">
<button
onClick={handleCopyMarkdown}
disabled={isLoading}
className="flex items-center gap-2 w-full p-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-left disabled:opacity-50"
>
<Copy className="size-4" />
Copy as Markdown for LLMs
</button>
<button
onClick={handleViewMarkdown}
disabled={isLoading}
className="flex items-center gap-2 w-full p-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-left disabled:opacity-50"
>
<FileText className="size-4" />
View as Markdown
</button>
<button
onClick={handleOpenInChatGPT}
disabled={isLoading}
className="flex items-center gap-2 w-full p-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-left disabled:opacity-50"
>
<MessageSquare className="size-4" />
Open in ChatGPT
</button>
<button
onClick={handleOpenInClaude}
disabled={isLoading}
className="flex items-center gap-2 w-full p-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-left disabled:opacity-50"
>
<ExternalLink className="size-4" />
Open in Claude
</button>
</div>
</Popover.Content>
</Popover.Root>
);
}
Loading