From 858d53fd2d03a9a059898f945518f5d2ff94c28a Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 00:56:19 +0100 Subject: [PATCH] auto-claude: subtask-4-5 - Create chatbot API route handler with streaming responses Implemented Next.js API route at /api/chat with: - POST endpoint for sending messages with streaming responses (SSE) - GET endpoint for retrieving chat history - OPTIONS endpoint for CORS preflight - Integration with Deepseek API via createStreamingChatCompletion - Session management via getOrCreateChatSession (24h continuity) - Message persistence to Supabase (user + assistant messages) - Context management with 20-message history limit - Error handling for API key/database issues - Request validation for required fields (message, visitorId) Co-Authored-By: Claude Opus 4.5 --- src/app/api/chat/route.ts | 238 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/app/api/chat/route.ts diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts new file mode 100644 index 0000000..861093b --- /dev/null +++ b/src/app/api/chat/route.ts @@ -0,0 +1,238 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { + createStreamingChatCompletion, + PORTFOLIO_SYSTEM_PROMPT, + type ChatMessage, +} from '@/lib/deepseek'; +import { + getOrCreateChatSession, + addChatMessage, + getConversationHistory, +} from '@/lib/supabase/chat'; + +// Maximum number of messages to include in context +const MAX_CONTEXT_MESSAGES = 20; + +// Request body interface +interface ChatRequestBody { + message: string; + visitorId: string; + sessionId?: string; +} + +// Validate request body +function validateRequestBody(body: unknown): body is ChatRequestBody { + if (!body || typeof body !== 'object') { + return false; + } + + const { message, visitorId } = body as Record; + + if (typeof message !== 'string' || message.trim().length === 0) { + return false; + } + + if (typeof visitorId !== 'string' || visitorId.trim().length === 0) { + return false; + } + + return true; +} + +// POST handler for chat messages +export async function POST(request: NextRequest) { + try { + // Parse request body + const body = await request.json(); + + // Validate request + if (!validateRequestBody(body)) { + return NextResponse.json( + { error: 'Invalid request. Required fields: message, visitorId' }, + { status: 400 } + ); + } + + const { message, visitorId } = body; + + // Get or create a chat session for this visitor + const session = await getOrCreateChatSession(visitorId, { + userAgent: request.headers.get('user-agent') ?? undefined, + locale: request.headers.get('accept-language') ?? undefined, + }); + + // Save the user message to database + await addChatMessage({ + session_id: session.id, + role: 'user', + content: message.trim(), + }); + + // Get conversation history for context + const history = await getConversationHistory(session.id, MAX_CONTEXT_MESSAGES); + + // Build messages array with system prompt and history + const messages: ChatMessage[] = [ + { role: 'system', content: PORTFOLIO_SYSTEM_PROMPT }, + ...history, + ]; + + // Create streaming response from Deepseek + const stream = await createStreamingChatCompletion({ + messages, + temperature: 0.7, + maxTokens: 1024, + }); + + // Collect the full response for database storage + let fullResponse = ''; + + // Create a TransformStream to handle the streaming response + const encoder = new TextEncoder(); + const readableStream = new ReadableStream({ + async start(controller) { + try { + for await (const chunk of stream) { + const content = chunk.choices[0]?.delta?.content ?? ''; + if (content) { + fullResponse += content; + // Send chunk as Server-Sent Event format + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ content, sessionId: session.id })}\n\n`) + ); + } + } + + // Save the complete assistant response to database + if (fullResponse.trim()) { + await addChatMessage({ + session_id: session.id, + role: 'assistant', + content: fullResponse.trim(), + }); + } + + // Send completion event + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ done: true, sessionId: session.id })}\n\n`) + ); + controller.close(); + } catch (streamError) { + // Handle streaming errors + const errorMessage = + streamError instanceof Error + ? streamError.message + : 'Streaming error occurred'; + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ error: errorMessage })}\n\n`) + ); + controller.close(); + } + }, + }); + + // Return streaming response with appropriate headers + return new Response(readableStream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + Connection: 'keep-alive', + 'X-Session-Id': session.id, + }, + }); + } catch (error) { + // Log error for debugging (only in development) + if (process.env.NODE_ENV === 'development') { + console.error('Chat API error:', error); + } + + // Handle specific error types + if (error instanceof Error) { + // Check for API key issues + if (error.message.includes('DEEPSEEK_API_KEY')) { + return NextResponse.json( + { error: 'Chat service is not configured' }, + { status: 503 } + ); + } + + // Check for Supabase issues + if ( + error.message.includes('SUPABASE') || + error.message.includes('Supabase') + ) { + return NextResponse.json( + { error: 'Database service is not available' }, + { status: 503 } + ); + } + } + + // Generic error response + return NextResponse.json( + { error: 'An error occurred while processing your message' }, + { status: 500 } + ); + } +} + +// GET handler for retrieving chat history +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const sessionId = searchParams.get('sessionId'); + const visitorId = searchParams.get('visitorId'); + + // Need either sessionId or visitorId + if (!sessionId && !visitorId) { + return NextResponse.json( + { error: 'Either sessionId or visitorId is required' }, + { status: 400 } + ); + } + + // If we have visitorId, get or create session + if (visitorId) { + const session = await getOrCreateChatSession(visitorId); + const history = await getConversationHistory(session.id, MAX_CONTEXT_MESSAGES); + + return NextResponse.json({ + sessionId: session.id, + messages: history, + }); + } + + // If we have sessionId, get history directly + if (sessionId) { + const history = await getConversationHistory(sessionId, MAX_CONTEXT_MESSAGES); + + return NextResponse.json({ + sessionId, + messages: history, + }); + } + + return NextResponse.json({ messages: [] }); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.error('Chat history API error:', error); + } + + return NextResponse.json( + { error: 'Failed to retrieve chat history' }, + { status: 500 } + ); + } +} + +// OPTIONS handler for CORS preflight +export async function OPTIONS() { + return new Response(null, { + status: 204, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +}