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:
@@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user