This commit adds comprehensive AI response streaming and critical deployment features: ## AI Streaming Implementation - **Backend StreamingService**: Token-by-token Azure OpenAI streaming (163 lines) - SSE endpoint at POST /api/v1/ai/chat/stream - Buffer management for incomplete SSE events - Stream callback architecture with chunk types (token, done, error) - **Frontend useStreamingChat Hook**: Fetch API with ReadableStream (127 lines) - Token accumulation with state management - Error handling and completion callbacks - **UI Integration**: Streaming message bubble with animated blinking cursor - Auto-scroll as tokens arrive - Loading indicator while waiting for first token - Seamless transition from streaming to completed message - **Safety Integration**: All safety checks preserved - Rate limiting and input sanitization - Context building reused from chat() method ## Deployment Infrastructure (Previous Session) - **Environment Configuration System**: - .env.example with 140+ configuration options - .env.staging and .env.production templates - Typed configuration service (environment.config.ts, 200 lines) - Environment-specific settings for DB, Redis, backups, AI - **Secret Management**: - Provider abstraction for AWS Secrets Manager, HashiCorp Vault, env vars - 5-minute caching with automatic refresh (secrets.service.ts, 189 lines) - Batch secret retrieval and validation - **Database Backup System**: - Automated PostgreSQL/MongoDB backups with cron scheduling - pg_dump + gzip compression, 30-day retention - S3 upload integration (backup.service.ts, 306 lines) - Admin endpoints for manual operations - Comprehensive documentation (BACKUP_STRATEGY.md, 343 lines) - **Health Check Monitoring**: - Kubernetes-ready health probes (liveness/readiness/startup) - Custom health indicators for Redis, MongoDB, MinIO, Azure OpenAI - Response time tracking (health.controller.ts, 108 lines) ## Files Modified - maternal-web/components/features/ai-chat/AIChatInterface.tsx - maternal-app/maternal-app-backend/src/modules/ai/ai.service.ts - maternal-app/maternal-app-backend/src/modules/ai/ai.module.ts - docs/implementation-gaps.md (updated feature counts: 62/128 complete, 48%) ## Files Created - maternal-web/hooks/useStreamingChat.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import apiClient from '@/lib/api/client';
|
|
|
|
export interface StreamChunk {
|
|
type: 'token' | 'metadata' | 'done' | 'error';
|
|
content?: string;
|
|
metadata?: any;
|
|
error?: string;
|
|
}
|
|
|
|
export interface ChatMessageDto {
|
|
message: string;
|
|
conversationId?: string;
|
|
language?: string;
|
|
}
|
|
|
|
/**
|
|
* Hook for streaming AI chat responses
|
|
* Uses Server-Sent Events (SSE) for real-time token streaming
|
|
*/
|
|
export function useStreamingChat() {
|
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const streamMessage = useCallback(
|
|
async (
|
|
chatDto: ChatMessageDto,
|
|
onChunk: (chunk: StreamChunk) => void,
|
|
onComplete?: () => void,
|
|
onError?: (error: string) => void
|
|
) => {
|
|
setIsStreaming(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/ai/chat/stream`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
// Add auth token if available
|
|
...(typeof window !== 'undefined' && localStorage.getItem('accessToken')
|
|
? { Authorization: `Bearer ${localStorage.getItem('accessToken')}` }
|
|
: {}),
|
|
},
|
|
body: JSON.stringify(chatDto),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const reader = response.body?.getReader();
|
|
if (!reader) {
|
|
throw new Error('Response body is not readable');
|
|
}
|
|
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
|
|
if (done) {
|
|
break;
|
|
}
|
|
|
|
// Decode the chunk and add to buffer
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
// Split by newlines to process complete SSE events
|
|
const lines = buffer.split('\n');
|
|
|
|
// Keep the last incomplete line in the buffer
|
|
buffer = lines.pop() || '';
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
|
|
// Skip empty lines
|
|
if (!trimmed) {
|
|
continue;
|
|
}
|
|
|
|
// Parse SSE data format
|
|
if (trimmed.startsWith('data: ')) {
|
|
const data = trimmed.substring(6);
|
|
|
|
try {
|
|
const chunk: StreamChunk = JSON.parse(data);
|
|
|
|
// Emit the chunk
|
|
onChunk(chunk);
|
|
|
|
// Check for completion
|
|
if (chunk.type === 'done') {
|
|
setIsStreaming(false);
|
|
if (onComplete) {
|
|
onComplete();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for errors
|
|
if (chunk.type === 'error') {
|
|
setIsStreaming(false);
|
|
const errorMsg = chunk.error || 'Streaming error occurred';
|
|
setError(errorMsg);
|
|
if (onError) {
|
|
onError(errorMsg);
|
|
}
|
|
return;
|
|
}
|
|
} catch (parseError) {
|
|
console.error('Failed to parse SSE chunk:', parseError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
const errorMsg = err instanceof Error ? err.message : 'Streaming failed';
|
|
setError(errorMsg);
|
|
setIsStreaming(false);
|
|
if (onError) {
|
|
onError(errorMsg);
|
|
}
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
return {
|
|
streamMessage,
|
|
isStreaming,
|
|
error,
|
|
};
|
|
}
|