diff --git a/apps/docs/src/app/docs/(default)/[[...slug]]/page.tsx b/apps/docs/src/app/docs/(default)/[[...slug]]/page.tsx index 1a70e9613e..e3e752d22d 100644 --- a/apps/docs/src/app/docs/(default)/[[...slug]]/page.tsx +++ b/apps/docs/src/app/docs/(default)/[[...slug]]/page.tsx @@ -13,6 +13,7 @@ import { PageLastUpdate, } from '@/components/layout/notebook/page'; import { TechArticleSchema, BreadcrumbSchema } from '@/components/structured-data'; +import { PageFeedback } from '@/components/page-feedback'; interface PageParams { slug?: string[]; @@ -68,6 +69,7 @@ export default async function Page({ /> )} + ); diff --git a/apps/docs/src/app/docs/(default)/layout.tsx b/apps/docs/src/app/docs/(default)/layout.tsx index d2cd123927..a726ff3b85 100644 --- a/apps/docs/src/app/docs/(default)/layout.tsx +++ b/apps/docs/src/app/docs/(default)/layout.tsx @@ -4,6 +4,7 @@ import { VersionSwitcher } from '@/components/version-switcher'; import type { LinkItemType } from 'fumadocs-ui/layouts/shared'; import { DocsLayout } from '@/components/layout/notebook'; import { LATEST_VERSION } from '@/lib/version'; +import { FloatingAsk } from '@/components/floating-ask'; export default async function Layout({ children, @@ -21,14 +22,17 @@ export default async function Layout({ ]; return ( - - {children} - + <> + + {children} + + + ); } diff --git a/apps/docs/src/components/floating-ask.tsx b/apps/docs/src/components/floating-ask.tsx new file mode 100644 index 0000000000..3b929a2e6d --- /dev/null +++ b/apps/docs/src/components/floating-ask.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useEffect, useCallback, useState } from 'react'; +import { ArrowUp } from 'lucide-react'; +import { cn } from '@prisma-docs/ui/lib/cn'; +import { useAIChatContext } from '@/hooks/use-ai-chat'; + +export function FloatingAsk() { + const { setIsOpen, setPrompt } = useAIChatContext(); + const [visible, setVisible] = useState(true); + const [inputValue, setInputValue] = useState(''); + const [isMac, setIsMac] = useState(true); + + useEffect(() => { + setIsMac(navigator.platform?.toUpperCase().includes('MAC') ?? true); + }, []); + + const checkScroll = useCallback(() => { + const distanceFromBottom = + document.documentElement.scrollHeight - + window.scrollY - + window.innerHeight; + setVisible(distanceFromBottom > 200); + }, []); + + useEffect(() => { + window.addEventListener('scroll', checkScroll, { passive: true }); + checkScroll(); + return () => window.removeEventListener('scroll', checkScroll); + }, [checkScroll]); + + const handleSubmit = () => { + if (inputValue.trim()) { + setPrompt(inputValue.trim()); + } + setIsOpen(true); + setInputValue(''); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + return ( +
+
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Ask a question..." + className="w-full bg-transparent text-sm text-fd-foreground placeholder:text-fd-muted-foreground focus:outline-none" + /> +
+ + {isMac ? '⌘' : 'Ctrl'}I + + +
+
+
+ ); +} diff --git a/apps/docs/src/components/layout/notebook/index.tsx b/apps/docs/src/components/layout/notebook/index.tsx index 75b1c15f71..16b445f372 100644 --- a/apps/docs/src/components/layout/notebook/index.tsx +++ b/apps/docs/src/components/layout/notebook/index.tsx @@ -44,6 +44,7 @@ import { type SidebarTabWithProps, } from '../sidebar/tabs/dropdown'; import { AIChatSidebar } from '@/components/ai-chat-sidebar'; +import { StatusIndicator } from '@/components/status-indicator'; export interface DocsLayoutProps extends BaseLayoutProps { tree: PageTree.Root; @@ -310,6 +311,9 @@ function DocsNavbar({ /> ))}
+
+ +
{links diff --git a/apps/docs/src/components/page-feedback.tsx b/apps/docs/src/components/page-feedback.tsx new file mode 100644 index 0000000000..4166a47170 --- /dev/null +++ b/apps/docs/src/components/page-feedback.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { ThumbsUp, ThumbsDown, Send } from 'lucide-react'; +import { cn } from '@prisma-docs/ui/lib/cn'; +import { usePathname } from 'fumadocs-core/framework'; +import posthog from 'posthog-js'; + +type FeedbackState = 'idle' | 'upvoted' | 'downvoted' | 'submitted'; + +function getStorageKey(path: string) { + return `prisma-docs:page-feedback:${path}`; +} + +export function PageFeedback() { + const pathname = usePathname(); + const [state, setState] = useState('idle'); + const [comment, setComment] = useState(''); + const [showTextarea, setShowTextarea] = useState(false); + + useEffect(() => { + try { + const stored = localStorage.getItem(getStorageKey(pathname)); + if (stored) setState('submitted'); + } catch {} + }, [pathname]); + + const persist = useCallback( + (vote: 'up' | 'down', text?: string) => { + try { + localStorage.setItem(getStorageKey(pathname), JSON.stringify({ vote, text })); + } catch {} + posthog.capture('docs:page_feedback', { + path: pathname, + vote, + comment: text ?? null, + }); + }, + [pathname], + ); + + const handleUp = () => { + setState('submitted'); + persist('up'); + }; + + const handleDown = () => { + setState('downvoted'); + setShowTextarea(true); + }; + + const handleSubmitComment = () => { + setState('submitted'); + persist('down', comment || undefined); + }; + + if (state === 'submitted') { + return ( +
+ Thanks for your feedback! +
+ ); + } + + return ( +
+
+ + Was this page helpful? + +
+ + +
+
+ + {showTextarea && ( +
+