Pārlūkot izejas kodu

chore(dev-hub) Open in LLM button

Aditya Arora 3 nedēļas atpakaļ
vecāks
revīzija
539ec125df

+ 1 - 1
apps/developer-hub/src/app/(docs)/[section]/[...slug]/page.tsx

@@ -1,7 +1,7 @@
-export { DocumentationPage as default } from "../../../../components/Pages/DocumentationPage";
 import type { Metadata } from "next";
 import { notFound } from "next/navigation";
 
+export { DocumentationPage as default } from "../../../../components/Pages/DocumentationPage";
 import { source } from "../../../../lib/source";
 
 export function generateStaticParams() {

+ 36 - 0
apps/developer-hub/src/app/llms.txt/route.ts

@@ -0,0 +1,36 @@
+import { NextResponse } from 'next/server';
+
+import { getLLMText } from '../../lib/get-llm-text';
+import { source } from '../../lib/source';
+
+export async function GET() {
+  try {
+    const pages = source.getPages();
+    const scan = pages.map((page) => getLLMText(page));
+    const scanned = await Promise.all(scan);
+    
+    const content = [
+      '# Pyth Documentation',
+      '',
+      'This file contains the complete Pyth documentation for LLM consumption.',
+      `Generated on: ${new Date().toISOString()}`,
+      '',
+      '## About Pyth',
+      'Pyth is a decentralized price oracle network that provides real-time price feeds for a wide range of assets.',
+      '',
+      '---',
+      '',
+      ...scanned,
+    ].join('\n');
+    
+    return new NextResponse(content, {
+      status: 200,
+      headers: {
+        'Content-Type': 'text/plain; charset=utf-8',
+        'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
+      },
+    });
+  } catch {
+    return new NextResponse('Internal server error', { status: 500 });
+  }
+}

+ 31 - 0
apps/developer-hub/src/app/mdx/[...slug]/route.ts

@@ -0,0 +1,31 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+import { getLLMText } from '../../../lib/get-llm-text';
+import { source } from '../../../lib/source';
+
+
+export async function GET(
+  request: NextRequest,
+  { params }: { params: Promise<{ slug: string[] }> }
+) {
+  try {
+    const { slug } = await params;
+    const page = source.getPage(slug);
+    
+    if (!page) {
+      return new NextResponse('Page not found', { status: 404 });
+    }
+    
+    const content = await getLLMText(page);
+    
+    return new NextResponse(content, {
+      status: 200,
+      headers: {
+        'Content-Type': 'text/plain; charset=utf-8',
+        'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
+      },
+    });
+  } catch {
+    return new NextResponse('Internal server error', { status: 500 });
+  }
+}

+ 183 - 0
apps/developer-hub/src/components/LLMShare/index.tsx

@@ -0,0 +1,183 @@
+'use client';
+
+import { Copy, Check, OpenAiLogo, Eye, FileText, ArrowSquareOut } from '@phosphor-icons/react/dist/ssr';
+import { Select } from '@pythnetwork/component-library/Select';
+import { useLogger } from '@pythnetwork/component-library/useLogger';
+import { useState } from 'react';
+
+import { ClaudeIcon } from '../../lib/icons';
+
+type LLMShareProps = {
+  content: string;
+  title: string;
+  url: string;
+};
+
+type DropdownOption = {
+  id: string;
+  name: string;
+  url?: string;
+  icon: React.ComponentType;
+  type: 'markdown' | 'llm';
+};
+
+const getDropdownOptions = (): DropdownOption[] => [
+  {
+    id: 'view-markdown',
+    name: 'View as Markdown',
+    icon: Eye,
+    type: 'markdown',
+  },
+  {
+    id: 'download-markdown',
+    name: 'Download Markdown',
+    icon: FileText,
+    type: 'markdown',
+  },
+  {
+    id: 'chatgpt',
+    name: 'Ask ChatGPT',
+    url: 'https://chat.openai.com',
+    icon: OpenAiLogo,
+    type: 'llm',
+  },
+  {
+    id: 'claude',
+    name: 'Ask Claude',
+    url: 'https://claude.ai',
+    icon: ClaudeIcon,
+    type: 'llm',
+  },
+];
+
+export function LLMShare({ content, title, url }: LLMShareProps) {
+  const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
+  const [selectedKey, setSelectedKey] = useState<string>('');
+  const logger = useLogger();
+  const dropdownOptions = getDropdownOptions();
+
+  async function handleCopy(key: string) {
+    try {
+      await navigator.clipboard.writeText(content);
+      setCopiedStates((prev) => ({ ...prev, [key]: true }));
+      setTimeout(() => {
+        setCopiedStates((prev) => ({ ...prev, [key]: false }));
+      }, 1200);
+    } catch (error) {
+      logger.error(error);
+    }
+  }
+
+  function handleDownloadMarkdown() {
+    const blob = new Blob([content], { type: 'text/markdown' });
+    const blobUrl = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = blobUrl;
+    const safeTitle = title.replaceAll(/[^a-zA-Z0-9]/g, '-');
+    a.download = `${safeTitle}.md`;
+    document.body.append(a);
+    a.click();
+    for (const anchor of document.body.querySelectorAll('a')) anchor.remove();
+    URL.revokeObjectURL(blobUrl);
+  }
+
+  function handleViewMarkdown() {
+    const blob = new Blob([content], { type: 'text/plain' });
+    const blobUrl = URL.createObjectURL(blob);
+    window.open(blobUrl, '_blank');
+    // Clean up the URL after a delay to ensure it opens
+    setTimeout(() => {
+      URL.revokeObjectURL(blobUrl);
+    }, 1000);
+  }
+
+  function handleShare(option: DropdownOption) {
+    if (option.type === 'llm' && option.url) {
+      const prompt = `Please read and analyze this documentation page:
+
+        Title: ${title}
+        URL: ${url}
+
+        Content:
+        ${content}
+
+        Please provide a summary and answer any questions I might have about this content.`;
+      
+      const encodedInstruction = encodeURIComponent(prompt);
+      const shareUrl =
+        option.name === 'Ask Claude'
+          ? `https://claude.ai/new?q=${encodedInstruction}`
+          : `${option.url}?q=${encodedInstruction}`;
+      
+      window.open(shareUrl, '_blank');
+    }
+  }
+
+  function handleSelectionChange(newKey: string | number) {
+    setSelectedKey(String(newKey));
+    const option = dropdownOptions.find((o) => o.id === newKey);
+    if (option) {
+      if (option.type === 'markdown') {
+        if (option.id === 'view-markdown') {
+          handleViewMarkdown();
+        } else if (option.id === 'download-markdown') {
+          handleDownloadMarkdown();
+        }
+      } else {
+        handleShare(option);
+      }
+      // Reset selection after action
+      setSelectedKey('');
+    }
+  }
+
+  return (
+    <div className="inline-flex rounded-md border border-border">
+      {/* Main copy button */}
+      <button
+        onClick={() => {
+          handleCopy('markdown').catch(() => { /* no-op */ });
+        }}
+        className="cursor-pointer relative inline-flex items-center gap-2 rounded-l-md rounded-r-none px-2 py-1.5 text-sm font-fono focus:z-10 border-0 shadow-none transition-all duration-150 hover:bg-neutral-150 dark:hover:bg-neutral-700"
+        aria-label="Copy page content"
+      >
+        <span
+          className={`inline-flex items-center justify-center rounded p-0.5 transition-all duration-150 ${
+            copiedStates.markdown ? '!text-brand-orange' : ''
+          }`}
+        >
+          {copiedStates.markdown ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
+        </span>
+        Copy page
+      </button>
+
+      {/* Divider */}
+      <div className="border-l border-border" />
+
+      {/* Select dropdown trigger */}
+      <div className="relative">
+        <Select<DropdownOption>
+          label="More options"
+          buttonLabel=""
+          hideLabel
+          size="sm"
+          variant="outline"
+          options={dropdownOptions}
+          selectedKey={selectedKey}
+          onSelectionChange={handleSelectionChange}
+          show={(option) => {
+            const Icon = option.icon;
+            return (
+              <div className="flex items-center gap-2">
+                <Icon />
+                <span>{option.name}</span>
+                {option.type === 'llm' && <ArrowSquareOut className="ml-auto h-3 w-3" />}
+              </div>
+            );
+          }}
+          textValue={(option) => option.name}
+        />
+      </div>
+    </div>
+  );
+}

+ 10 - 2
apps/developer-hub/src/components/Pages/BasePage/index.tsx

@@ -6,18 +6,26 @@ import {
 } from "fumadocs-ui/page";
 import { notFound } from "next/navigation";
 
+import { getLLMText } from "../../../lib/get-llm-text";
 import { source } from "../../../lib/source";
 import { getMDXComponents } from "../../../mdx-components";
+import { LLMShare } from "../../LLMShare";
 
-export function BasePage(props: { params: { slug: string[] } }) {
+export async function BasePage(props: { params: { slug: string[] } }) {
   const page = source.getPage(props.params.slug);
   if (!page) notFound();
 
   const MDX = page.data.body;
+  const content = await getLLMText(page);
+  const title = page.data.title;
+  const url = page.url;
 
   return (
     <DocsPage toc={page.data.toc} full={page.data.full}>
-      <DocsTitle>{page.data.title}</DocsTitle>
+      <div className="flex items-center justify-between gap-4">
+        <DocsTitle>{page.data.title}</DocsTitle>
+        <LLMShare content={content} title={title} url={url} />
+      </div>
       <DocsDescription>{page.data.description}</DocsDescription>
       <DocsBody>
         <MDX components={getMDXComponents()} />

+ 4 - 7
apps/developer-hub/src/lib/get-llm-text.ts

@@ -1,23 +1,20 @@
 import fs from "node:fs/promises";
 
-import type { InferPageType } from "fumadocs-core/source";
+import type { Page } from "fumadocs-core/source";
 import { remarkInclude } from "fumadocs-mdx/config";
 import { remark } from "remark";
 import remarkGfm from "remark-gfm";
 import remarkMdx from "remark-mdx";
 
-import { source } from "./source";
-
 const processor = remark().use(remarkMdx).use(remarkInclude).use(remarkGfm);
 
-export async function getLLMText(page: InferPageType<typeof source>) {
+export async function getLLMText(page: Page) {
   const processed = await processor.process({
     path: page.path,
-    value: await fs.readFile(page.path, "utf8"),
+    value: await fs.readFile(page.absolutePath, "utf8"),
   });
 
-  // note: it doesn't escape frontmatter, it's up to you.
-  return `# ${page.data.title}
+  return `# ${page.data.title ?? "Untitled"}
 URL: ${page.url}
 
 ${String(processed.value)}`;

+ 32 - 0
apps/developer-hub/src/lib/icons.tsx

@@ -0,0 +1,32 @@
+import type { ComponentProps } from "react";
+import { createElement } from "react";
+
+export function ClaudeIcon({
+  size = 24,
+  color = "currentColor",
+  className,
+  style,
+  ...props
+}: ComponentProps<"svg"> & {
+  size?: number;
+  color?: string;
+}) {
+  return createElement(
+    "svg",
+    {
+      role: "img",
+      width: size,
+      height: size,
+      viewBox: "0 0 24 24",
+      xmlns: "http://www.w3.org/2000/svg",
+      fill: color,
+      className,
+      style,
+      ...props,
+    },
+    createElement("path", {
+      d: "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z",
+    }),
+  );
+}
+