mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Discord webhook integration for ai chat
This commit is contained in:
parent
fc6783cfcb
commit
cb01e0da4a
148
docs/lib/discord-webhook.ts
Normal file
148
docs/lib/discord-webhook.ts
Normal file
@ -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}`;
|
||||
}
|
||||
20
docs/src/app/api/discord-webhook/route.ts
Normal file
20
docs/src/app/api/discord-webhook/route.ts
Normal file
@ -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 });
|
||||
}
|
||||
}
|
||||
@ -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 */}
|
||||
<div className="border-t border-fd-border p-3">
|
||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||
<form onSubmit={handleChatSubmit} className="flex gap-2">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
@ -261,7 +351,7 @@ export function AIChatDrawer() {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e as React.FormEvent);
|
||||
handleChatSubmit(e as React.FormEvent);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user