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 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 00:56:19 +01:00
co-authored by Claude Opus 4.5
parent f8fbac8ac6
commit 858d53fd2d
+238
View File
@@ -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<string, unknown>;
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',
},
});
}