diff --git a/docs/lib/discord-webhook.ts b/docs/lib/discord-webhook.ts new file mode 100644 index 000000000..2c9439ed6 --- /dev/null +++ b/docs/lib/discord-webhook.ts @@ -0,0 +1,148 @@ +/** + * Sends a message to Discord webhook with rich metadata + */ +export async function sendToDiscordWebhook(data: { + message: string; + username?: string; + metadata?: { + url?: string; + pathname?: string; + timestamp?: string; + userAgent?: string; + viewport?: string; + isHomePage?: boolean; + isScrolled?: boolean; + messageLength?: number; + messageType?: string; + sessionMessageCount?: number; + timeOnPage?: number; + scrollDepth?: number; + referrer?: string; + language?: string; + timezone?: string; + chatExpanded?: boolean; + }; +}) { + const webhookUrl = process.env.DISCORD_WEBHOOK_URL; + + if (!webhookUrl) { + console.warn('Discord webhook URL not configured'); + return; + } + + try { + const { message, username, metadata } = data; + + // Create a rich embed with metadata + const embed = { + title: "💬 AI Chat Message", + description: message.length > 100 ? message.substring(0, 100) + "..." : message, + color: metadata?.messageType === 'starter-prompt' ? 0x10b981 : 0x3b82f6, // Green for starter prompts, blue for custom + fields: [ + // Message Info + ...(metadata?.messageType ? [{ name: "📝 Type", value: metadata.messageType === 'starter-prompt' ? "Starter Prompt" : "Custom Message", inline: true }] : []), + ...(metadata?.messageLength ? [{ name: "📏 Length", value: `${metadata.messageLength} chars`, inline: true }] : []), + ...(metadata?.chatExpanded !== undefined ? [{ name: "🔍 Chat Mode", value: metadata.chatExpanded ? "Expanded" : "Normal", inline: true }] : []), + + // Page Context + ...(metadata?.pathname ? [{ name: "📄 Page", value: metadata.pathname, inline: true }] : []), + ...(metadata?.isHomePage !== undefined ? [{ name: "🏠 Homepage", value: metadata.isHomePage ? "Yes" : "No", inline: true }] : []), + ...(metadata?.isScrolled !== undefined ? [{ name: "📜 Scrolled", value: metadata.isScrolled ? "Yes" : "No", inline: true }] : []), + + // Session Data + ...(metadata?.sessionMessageCount ? [{ name: "💬 Session Messages", value: metadata.sessionMessageCount.toString(), inline: true }] : []), + ...(metadata?.timeOnPage ? [{ name: "⏱️ Time on Page", value: formatTime(metadata.timeOnPage), inline: true }] : []), + ...(metadata?.scrollDepth !== undefined ? [{ name: "📊 Scroll Depth", value: `${metadata.scrollDepth}%`, inline: true }] : []), + + // Technical Info + ...(metadata?.viewport ? [{ name: "📱 Viewport", value: metadata.viewport, inline: true }] : []), + ...(metadata?.language ? [{ name: "🌐 Language", value: metadata.language, inline: true }] : []), + ...(metadata?.timezone ? [{ name: "⏰ Timezone", value: metadata.timezone, inline: true }] : []), + ], + timestamp: metadata?.timestamp || new Date().toISOString(), + footer: { + text: "Stack Auth Docs AI Chat", + icon_url: "https://cdn.discordapp.com/embed/avatars/0.png" + } + }; + + // Add full message as a separate field if it was truncated + if (message.length > 100) { + embed.fields.unshift({ name: "📝 Full Message", value: message, inline: false }); + } + + // Extract browser info from user agent + const browserInfo = extractBrowserInfo(metadata?.userAgent || ''); + if (browserInfo) { + embed.fields.push({ name: "🌐 Browser", value: browserInfo, inline: true }); + } + + // Add referrer info + if (metadata?.referrer && metadata.referrer !== 'Direct') { + embed.fields.push({ name: "🔗 Referrer", value: metadata.referrer, inline: true }); + } + + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: username || 'Stack Auth Docs User', + avatar_url: 'https://cdn.discordapp.com/embed/avatars/0.png', + embeds: [embed], + }), + }); + + if (!response.ok) { + console.error('Failed to send message to Discord:', response.statusText); + } + } catch (error) { + console.error('Error sending message to Discord:', error); + } +} + +/** + * Format time in seconds to a human-readable format + */ +function formatTime(seconds: number): string { + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; +} + +/** + * Extract browser and OS info from user agent + */ +function extractBrowserInfo(userAgent: string): string | null { + if (!userAgent) return null; + + // Extract browser + let browser = 'Unknown'; + if (userAgent.includes('Chrome/')) { + browser = 'Chrome'; + } else if (userAgent.includes('Firefox/')) { + browser = 'Firefox'; + } else if (userAgent.includes('Safari/') && !userAgent.includes('Chrome/')) { + browser = 'Safari'; + } else if (userAgent.includes('Edge/')) { + browser = 'Edge'; + } + + // Extract OS + let os = 'Unknown'; + if (userAgent.includes('Windows NT')) { + os = 'Windows'; + } else if (userAgent.includes('Mac OS X')) { + os = 'macOS'; + } else if (userAgent.includes('Linux')) { + os = 'Linux'; + } else if (userAgent.includes('iPhone') || userAgent.includes('iPad')) { + os = 'iOS'; + } else if (userAgent.includes('Android')) { + os = 'Android'; + } + + return `${browser} on ${os}`; +} diff --git a/docs/src/app/api/discord-webhook/route.ts b/docs/src/app/api/discord-webhook/route.ts new file mode 100644 index 000000000..5ce048241 --- /dev/null +++ b/docs/src/app/api/discord-webhook/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { sendToDiscordWebhook } from '../../../../lib/discord-webhook'; + +export async function POST(request: NextRequest) { + try { + const data = await request.json(); + + if (!data.message || typeof data.message !== 'string') { + return NextResponse.json({ error: 'Message is required' }, { status: 400 }); + } + + // Send message to Discord webhook with all metadata + await sendToDiscordWebhook(data); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error in Discord webhook API:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/docs/src/components/chat/ai-chat.tsx b/docs/src/components/chat/ai-chat.tsx index ec7c06889..64a224923 100644 --- a/docs/src/components/chat/ai-chat.tsx +++ b/docs/src/components/chat/ai-chat.tsx @@ -34,6 +34,35 @@ export function AIChatDrawer() { const [docsContent, setDocsContent] = useState(''); const [isHomePage, setIsHomePage] = useState(false); const [isScrolled, setIsScrolled] = useState(false); + const [pageLoadTime] = useState(Date.now()); + const [sessionData, setSessionData] = useState({ + messagesCount: 0, + starterPromptsUsed: 0, + timeOnPage: 0, + scrollDepth: 0, + }); + + // Track session data + useEffect(() => { + const updateSessionData = () => { + const timeOnPage = Math.floor((Date.now() - pageLoadTime) / 1000); + const scrollDepth = Math.floor((window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100); + + setSessionData(prev => ({ + ...prev, + timeOnPage, + scrollDepth: Math.max(prev.scrollDepth, scrollDepth || 0), + })); + }; + + const interval = setInterval(updateSessionData, 5000); // Update every 5 seconds + window.addEventListener('scroll', updateSessionData); + + return () => { + clearInterval(interval); + window.removeEventListener('scroll', updateSessionData); + }; + }, [pageLoadTime]); // Detect if we're on homepage and scroll state useEffect(() => { @@ -112,6 +141,67 @@ export function AIChatDrawer() { }, }); + // Function to send message to Discord webhook + const sendToDiscord = async (message: string) => { + try { + // Gather additional context + const context = { + message: message, + username: 'Stack Auth Docs User', + metadata: { + url: window.location.href, + pathname: window.location.pathname, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent, + viewport: `${window.innerWidth}x${window.innerHeight}`, + isHomePage: isHomePage, + isScrolled: isScrolled, + messageLength: message.length, + messageType: starterPrompts.some(p => p.prompt === message) ? 'starter-prompt' : 'custom', + sessionMessageCount: messages.length + 1, + // Enhanced session data + timeOnPage: sessionData.timeOnPage, + scrollDepth: sessionData.scrollDepth, + referrer: document.referrer || 'Direct', + language: navigator.language, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + chatExpanded: isChatExpanded, + } + }; + + await fetch('/api/discord-webhook', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(context), + }); + } catch (error) { + console.error('Failed to send message to Discord:', error); + } + }; + + // Enhanced submit handler that also sends to Discord + const handleChatSubmit = async (e: React.FormEvent) => { + if (!input.trim()) return; + + // Update session data + const isStarterPrompt = starterPrompts.some(p => p.prompt === input.trim()); + setSessionData(prev => ({ + ...prev, + messagesCount: prev.messagesCount + 1, + starterPromptsUsed: prev.starterPromptsUsed + (isStarterPrompt ? 1 : 0), + })); + + // Send message to Discord webhook + sendToDiscord(input.trim()).catch(error => { + console.error('Discord webhook error:', error); + }); + + // Continue with normal chat submission + handleSubmit(e); + }; + // Starter prompts for users const starterPrompts = [ { @@ -250,7 +340,7 @@ export function AIChatDrawer() { {/* Input */}