diff --git a/docs/src/app/(home)/page.tsx b/docs/src/app/(home)/page.tsx index 7e5b832c6..e0af3b40c 100644 --- a/docs/src/app/(home)/page.tsx +++ b/docs/src/app/(home)/page.tsx @@ -1,4 +1,3 @@ -import AIChat from '@/components/chat/ai-chat'; import DocsSelector from '@/components/homepage/iconHover'; export default function HomePage() { @@ -35,11 +34,6 @@ export default function HomePage() {
- - {/* AI Chat Assistant */} -
- -
diff --git a/docs/src/app/api/chat/route.ts b/docs/src/app/api/chat/route.ts index 842c9a66e..4fe351d90 100644 --- a/docs/src/app/api/chat/route.ts +++ b/docs/src/app/api/chat/route.ts @@ -1,118 +1,97 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google'; -import { streamText } from 'ai'; +import { streamText, tool } from 'ai'; +import { z } from 'zod'; // Allow streaming responses up to 30 seconds export const maxDuration = 30; -// Configure Google Gemini with custom API key variable +// Create Google AI instance const google = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_AI_API_KEY, }); -export function errorHandler(error: unknown) { - if (error == null) { - return 'unknown error'; - } - - if (typeof error === 'string') { - return error; - } - +// Helper function to get error message +function getErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } - - return JSON.stringify(error); + return String(error); } -async function getStackAuthDocs() { +export async function POST(request: Request) { + const { messages, docsContent } = await request.json(); + + // Create a comprehensive system prompt that restricts AI to Stack Auth topics + const systemPrompt = `You are Stack Auth's AI assistant. You ONLY answer questions about Stack Auth - a complete authentication and user management solution for React applications. + +DOCUMENTATION CONTEXT: +${docsContent || 'Documentation not available'} + +STRICT GUIDELINES: +1. ONLY answer questions related to Stack Auth, its features, implementation, or usage +2. If asked about non-Stack Auth topics, politely redirect: "I can only help with Stack Auth questions. Please ask about Stack Auth's features, setup, or implementation." +3. Provide detailed, technical answers with code examples when relevant +4. Reference specific Stack Auth features, components, or APIs +5. When explaining concepts, always relate them to Stack Auth specifically +6. Include relevant code snippets from the documentation when helpful +7. If you're unsure about something Stack Auth-related, say so rather than guessing + +RESPONSE FORMAT: +- Use markdown formatting for better readability +- Include code blocks with proper syntax highlighting +- Use bullet points for lists +- Bold important concepts +- Provide practical examples when possible + +Remember: You are Stack Auth's dedicated assistant. Stay focused on Stack Auth topics only.`; + try { - // Get the base URL from the request or use localhost for development - const baseUrl = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : 'http://localhost:8104'; - - console.log('Fetching docs from:', `${baseUrl}/llms.txt`); - - const response = await fetch(`${baseUrl}/llms.txt`); - console.log('Docs fetch response status:', response.status); - - if (!response.ok) { - console.error('Failed to fetch Stack Auth docs:', response.status, response.statusText); - return null; - } - - const docsContent = await response.text(); - console.log('Docs content length:', docsContent?.length || 0); - console.log('Docs content preview:', docsContent?.substring(0, 200) + '...'); - - return docsContent; - } catch (error) { - console.error('Error fetching Stack Auth docs:', error); - return null; - } -} - -export async function POST(req: Request) { - try { - const { messages } = await req.json(); - - console.log('Received messages:', messages); - console.log('Google AI API Key configured:', !!process.env.GOOGLE_AI_API_KEY); - - // Fetch Stack Auth documentation - const stackAuthDocs = await getStackAuthDocs(); - - // Create system message with documentation context - const systemMessage = { - role: 'system' as const, - content: `You are a technical AI assistant specializing in Stack Auth, a complete authentication solution. You are helping developers who want detailed, technical guidance. - -IMPORTANT INSTRUCTIONS: -- You can ONLY answer questions about Stack Auth and authentication topics -- If someone asks about anything else, politely redirect them to ask about Stack Auth -- Your audience is DEVELOPERS who need in-depth technical information -- Provide comprehensive, detailed answers with code examples when available -- Include specific implementation details, configuration options, and best practices -- Reference exact function names, parameters, and code snippets from the documentation -- Don't oversimplify - developers want the full technical depth -- When explaining concepts, include relevant code examples and implementation details -- If there are multiple approaches, explain the different options and their trade-offs - -${stackAuthDocs ? ` -Here is the complete Stack Auth documentation with detailed examples and technical information: - -${stackAuthDocs} - -Use this documentation to provide comprehensive, technical answers. Include code examples, configuration details, and implementation specifics. Developers are looking for actionable, detailed guidance, not basic overviews. -` : 'Stack Auth documentation could not be loaded. Please answer based on general Stack Auth knowledge, but provide detailed technical information for developers.'} - -Remember: Your responses should match the technical depth and detail level of the Stack Auth documentation. Provide code examples, configuration snippets, and comprehensive implementation guidance.` - }; - - // Prepend system message to the conversation - const messagesWithContext = [systemMessage, ...messages]; - const result = streamText({ model: google('gemini-1.5-flash'), - messages: messagesWithContext, + system: systemPrompt, + messages, + maxTokens: 1000, + temperature: 0.3, + tools: { + searchDocs: tool({ + description: 'Search through Stack Auth documentation for specific information', + parameters: z.object({ + query: z.string().describe('The search query to find relevant documentation'), + }), + execute: async ({ query }) => { + // Simple search through the docs content + if (!docsContent) { + return 'Documentation not available'; + } + + const lines = docsContent.split('\n'); + const relevantLines = lines.filter((line: string) => + line.toLowerCase().includes(query.toLowerCase()) + ); + + if (relevantLines.length === 0) { + return `No specific information found for "${query}" in the documentation.`; + } + + return relevantLines.slice(0, 10).join('\n'); + }, + }), + }, }); return result.toDataStreamResponse({ - getErrorMessage: errorHandler, + getErrorMessage, }); } catch (error) { - console.error('Error in chat API:', error); + console.error('Chat API Error:', error); return new Response( - JSON.stringify({ - error: 'Internal server error', - details: error instanceof Error ? error.message : 'Unknown error' + JSON.stringify({ + error: 'Failed to process chat request', + details: getErrorMessage(error), }), { status: 500, - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, } ); } diff --git a/docs/src/app/global.css b/docs/src/app/global.css index 705c23b7d..4a7745198 100644 --- a/docs/src/app/global.css +++ b/docs/src/app/global.css @@ -5,3 +5,63 @@ @import '../components/mdx/mdx-info.css'; @import '../components/mdx/mdx-cards.css'; @import '../components/mdx/reset-code-styles.css'; + +/* Chat drawer content shifting for docs pages */ +body.chat-open:not(.home-page) { + padding-right: 25rem; /* 384px + 16px for spacing */ + transition: padding-right 300ms ease-out; +} + +/* Ensure smooth transition when closing for docs pages */ +body:not(.home-page) { + transition: padding-right 300ms ease-out; +} + +/* Chat drawer content shifting for homepage - much less padding */ +body.home-page.chat-open main { + padding-right: 12rem; /* Much less padding for homepage */ + transition: padding-right 300ms ease-out; +} + +/* Ensure smooth transition when closing for homepage main content */ +body.home-page main { + transition: padding-right 300ms ease-out; +} + +/* Special chat drawer positioning for homepage */ +body.home-page [data-chat-drawer] { + /* Start from top when pill is not visible */ + top: 56px; /* Same as top-14 */ + height: calc(100vh - 56px); + transition: top 300ms ease-out, height 300ms ease-out; +} + +/* When scrolled on homepage, adjust for pill navbar */ +body.home-page.scrolled [data-chat-drawer] { + top: 0; + height: 100vh; +} + +/* Flat scrollbar design for compact codeblocks */ +.compact-codeblock-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.compact-codeblock-scrollbar::-webkit-scrollbar-track { + background: transparent; +} + +.compact-codeblock-scrollbar::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 0px; + border: none; +} + +.compact-codeblock-scrollbar::-webkit-scrollbar-thumb:hover { + background: #525252; +} + +.compact-codeblock-scrollbar::-webkit-scrollbar-corner { + background: transparent; +} diff --git a/docs/src/app/layout.tsx b/docs/src/app/layout.tsx index 350867271..504d186b5 100644 --- a/docs/src/app/layout.tsx +++ b/docs/src/app/layout.tsx @@ -1,6 +1,7 @@ import { StackProvider, StackTheme } from '@stackframe/stack'; import { RootProvider } from 'fumadocs-ui/provider'; import { Inter } from 'next/font/google'; +import { ChatProvider } from '../components/chat/ai-chat'; import { stackServerApp } from '../stack'; import './global.css'; @@ -24,7 +25,9 @@ export default function Layout({ children }: { children: React.ReactNode }) { enabled: false, // Completely disable fumadocs search }} > - {children} + + {children} + diff --git a/docs/src/components/chat/ai-chat.tsx b/docs/src/components/chat/ai-chat.tsx index 9aa146a53..d029fdc35 100644 --- a/docs/src/components/chat/ai-chat.tsx +++ b/docs/src/components/chat/ai-chat.tsx @@ -1,140 +1,332 @@ 'use client'; import { useChat } from '@ai-sdk/react'; -import { Bot, Send, User } from 'lucide-react'; -import { useEffect, useState } from 'react'; +import { Maximize2, Minimize2, Send, X } from 'lucide-react'; +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; +import { MessageFormatter } from './message-formatter'; -export default function AIChat() { - const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat(); +// Stack Auth Icon Component (just the icon, not full logo) +function StackIcon({ size = 20, className }: { size?: number, className?: string }) { + return ( + + + + ); +} + +// Chat Context +type ChatContextType = { + isOpen: boolean, + isExpanded: boolean, + toggleChat: () => void, + expandChat: () => void, + collapseChat: () => void, +}; + +type ChatProviderProps = { + children: ReactNode, +}; + +const ChatContext = createContext(undefined); + +export function ChatProvider({ children }: ChatProviderProps) { const [isOpen, setIsOpen] = useState(false); - const [isHydrated, setIsHydrated] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); - // Prevent hydration mismatch by only rendering after client hydration + // Load state from localStorage on mount useEffect(() => { - setIsHydrated(true); + const savedIsOpen = localStorage.getItem('ai-chat-open'); + const savedIsExpanded = localStorage.getItem('ai-chat-expanded'); + + if (savedIsOpen === 'true') { + setIsOpen(true); + } + if (savedIsExpanded === 'true') { + setIsExpanded(true); + } }, []); - // Debug logging - console.log('Messages:', messages); + // Save state to localStorage when it changes + useEffect(() => { + localStorage.setItem('ai-chat-open', isOpen.toString()); + }, [isOpen]); - // Don't render until hydrated to prevent SSR/client mismatch - if (!isHydrated) { - return ( -
-
-
- - Loading AI Assistant... -
-
-
- ); - } + useEffect(() => { + localStorage.setItem('ai-chat-expanded', isExpanded.toString()); + }, [isExpanded]); + + // Add/remove body classes for content shifting + useEffect(() => { + if (isOpen) { + document.body.classList.add('chat-open'); + } else { + document.body.classList.remove('chat-open'); + setIsExpanded(false); // Close expansion when chat closes + } + + return () => { + document.body.classList.remove('chat-open'); + }; + }, [isOpen]); + + const toggleChat = () => setIsOpen(!isOpen); + const expandChat = () => setIsExpanded(true); + const collapseChat = () => setIsExpanded(false); + + const value = { + isOpen, + isExpanded, + toggleChat, + expandChat, + collapseChat, + }; return ( -
- {/* Toggle Button */} -
- + + {children} + + + ); +} + +export function useChatContext() { + const context = useContext(ChatContext); + if (context === undefined) { + throw new Error('useChatContext must be used within a ChatProvider'); + } + return context; +} + +function AIChatDrawer() { + const { isOpen, isExpanded, toggleChat, expandChat, collapseChat } = useChatContext(); + const [input, setInput] = useState(''); + const [docsContent, setDocsContent] = useState(''); + + // Fetch documentation content when component mounts + useEffect(() => { + let isCancelled = false; + + const fetchDocs = async () => { + try { + const response = await fetch('/llms.txt'); + if (response.ok && !isCancelled) { + const content = await response.text(); + setDocsContent(content); + } + } catch (error) { + console.error('Failed to fetch documentation:', error); + } + }; + + fetchDocs().catch((error) => { + console.error('Failed to fetch documentation:', error); + }); + + return () => { + isCancelled = true; + }; + }, []); + + const { + messages, + handleSubmit, + isLoading, + error, + } = useChat({ + api: '/api/chat', + initialMessages: [], + body: { + docsContent, + }, + onError: (error: Error) => { + console.error('Chat error:', error); + }, + }); + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() || isLoading) return; + + handleSubmit(e, { + body: { + docsContent, + }, + }); + setInput(''); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setInput(e.target.value); + }; + + // Starter prompts for users + const starterPrompts = [ + { + title: "Getting Started", + description: "Setup and installation", + prompt: "How do I get started with Stack Auth?" + }, + { + title: "Next.js Integration", + description: "Framework setup", + prompt: "How do I implement authentication in Next.js?" + }, + { + title: "Authentication Methods", + description: "Available options", + prompt: "What authentication methods does Stack Auth support?" + } + ]; + + const handleStarterPromptClick = (prompt: string) => { + setInput(prompt); + }; + + return ( +
+ {/* Header */} +
+
+ +
+

Stack Auth AI

+

Documentation assistant

+
+
+
+ {/* Expand/Collapse Button */} + + {/* Close Button */} + +
- {/* Chat Interface */} - {isOpen && ( -
-
- -

Stack Auth AI Assistant

- - Ask questions about Stack Auth documentation - + {/* Messages */} +
+ {messages.length === 0 ? ( +
+ +

How can I help?

+

+ Ask me about Stack Auth while you browse the docs. +

+
+ {starterPrompts.map((starter, index) => ( + + ))} +
- - {/* Messages Container */} -
- {messages.length === 0 && ( -
- -

Welcome to Stack Auth!

-

- Ask me anything about authentication, documentation, or how to get started. -

-
- )} - - {messages.map(message => ( + ) : ( + messages.map((message) => ( +
- {message.role === 'assistant' && ( -
- -
- )} - -
-
+ {message.role === 'user' ? ( +
{message.content}
-
- - {message.role === 'user' && ( -
- -
+ ) : ( + )}
- ))} +
+ )) + )} - {isLoading && ( -
-
- -
-
-
-
-
-
-
+ {isLoading && ( +
+
+
+
+
+
+
+ Thinking...
- )} +
+ )} - {/* Input Form */} -
- - -
-
- )} + {error && ( +
+ Error: {error.message} +
+ )} +
+ + {/* Input */} +
+
+