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