cidadao.ai-backend / docs /development /FRONTEND_INTEGRATION_GUIDE.md
anderson-ufrj
refactor: complete repository reorganization and documentation update
92d464e
|
raw
history blame
57.5 kB

🚀 Guia Completo de Integração Frontend - Cidadão.AI Backend API

Versão: 1.0.0
Última Atualização: Janeiro 2025
Backend URL: https://neural-thinker-cidadao-ai-backend.hf.space/
Documentação Interativa: https://neural-thinker-cidadao-ai-backend.hf.space/docs

📋 Índice

  1. Visão Geral
  2. Configuração Inicial
  3. Autenticação
  4. Endpoints Principais
  5. WebSockets e Real-time
  6. Exemplos de Implementação
  7. TypeScript Interfaces
  8. Tratamento de Erros
  9. Rate Limiting
  10. Boas Práticas

🎯 Visão Geral

O Cidadão.AI é uma plataforma multi-agente de IA para análise de transparência governamental brasileira. O backend fornece APIs RESTful, WebSockets e Server-Sent Events (SSE) para comunicação em tempo real.

Características Principais

  • Autenticação JWT com refresh tokens
  • Rate Limiting por tiers (Free, Basic, Premium, Enterprise)
  • WebSockets para comunicação bidirecional
  • SSE para streaming de respostas
  • 17 Agentes de IA especializados com identidades brasileiras
  • Cache otimizado com hit rate >90%
  • Tempo de resposta <2s para agentes

Base URLs

// Produção
const API_BASE_URL = 'https://neural-thinker-cidadao-ai-backend.hf.space'

// Development (local)
const API_BASE_URL_DEV = 'http://localhost:8000'

// WebSocket
const WS_BASE_URL = 'wss://neural-thinker-cidadao-ai-backend.hf.space'
const WS_BASE_URL_DEV = 'ws://localhost:8000'

🔧 Configuração Inicial

1. Instalação de Dependências

# Axios para requisições HTTP
npm install axios

# Socket.io para WebSockets (opcional - pode usar native WebSocket)
npm install socket.io-client

# Event Source Polyfill para SSE
npm install eventsource

# TypeScript types
npm install -D @types/eventsource

2. Configuração do Cliente HTTP

// utils/api-client.ts
import axios, { AxiosInstance } from 'axios'

class ApiClient {
  private client: AxiosInstance
  private refreshingToken: Promise<string> | null = null

  constructor() {
    this.client = axios.create({
      baseURL: process.env.NEXT_PUBLIC_API_URL || 'https://neural-thinker-cidadao-ai-backend.hf.space',
      headers: {
        'Content-Type': 'application/json',
      },
      timeout: 30000, // 30 segundos
    })

    // Request interceptor para adicionar token
    this.client.interceptors.request.use(
      async (config) => {
        const token = this.getAccessToken()
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (error) => Promise.reject(error)
    )

    // Response interceptor para refresh token
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config

        if (error.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true
          
          try {
            await this.refreshToken()
            const newToken = this.getAccessToken()
            originalRequest.headers.Authorization = `Bearer ${newToken}`
            return this.client(originalRequest)
          } catch (refreshError) {
            // Redirecionar para login
            window.location.href = '/login'
            return Promise.reject(refreshError)
          }
        }

        return Promise.reject(error)
      }
    )
  }

  private getAccessToken(): string | null {
    return localStorage.getItem('access_token')
  }

  private getRefreshToken(): string | null {
    return localStorage.getItem('refresh_token')
  }

  private async refreshToken(): Promise<string> {
    if (this.refreshingToken) {
      return this.refreshingToken
    }

    this.refreshingToken = this.client
      .post('/auth/refresh', {
        refresh_token: this.getRefreshToken(),
      })
      .then((response) => {
        const { access_token, refresh_token } = response.data
        localStorage.setItem('access_token', access_token)
        localStorage.setItem('refresh_token', refresh_token)
        this.refreshingToken = null
        return access_token
      })
      .catch((error) => {
        this.refreshingToken = null
        throw error
      })

    return this.refreshingToken
  }

  // Métodos públicos
  async get<T>(url: string, config?: any): Promise<T> {
    const response = await this.client.get<T>(url, config)
    return response.data
  }

  async post<T>(url: string, data?: any, config?: any): Promise<T> {
    const response = await this.client.post<T>(url, data, config)
    return response.data
  }

  async put<T>(url: string, data?: any, config?: any): Promise<T> {
    const response = await this.client.put<T>(url, data, config)
    return response.data
  }

  async delete<T>(url: string, config?: any): Promise<T> {
    const response = await this.client.delete<T>(url, config)
    return response.data
  }
}

export const apiClient = new ApiClient()

🔐 Autenticação

Fluxo de Autenticação

  1. Login → Recebe access_token e refresh_token
  2. Armazenamento → Salvar tokens no localStorage/cookies seguros
  3. Uso → Enviar access_token no header Authorization
  4. Refresh → Quando access_token expira, usar refresh_token
  5. Logout → Limpar tokens e chamar endpoint de logout

Endpoints de Autenticação

1. Login

// POST /auth/login
interface LoginRequest {
  email: string
  password: string
}

interface LoginResponse {
  access_token: string
  refresh_token: string
  token_type: string
  expires_in: number
  user: {
    id: string
    email: string
    name: string
    role: string
    is_active: boolean
  }
}

// Exemplo de uso
async function login(email: string, password: string): Promise<LoginResponse> {
  const response = await apiClient.post<LoginResponse>('/auth/login', {
    email,
    password
  })
  
  // Salvar tokens
  localStorage.setItem('access_token', response.access_token)
  localStorage.setItem('refresh_token', response.refresh_token)
  localStorage.setItem('user', JSON.stringify(response.user))
  
  return response
}

2. Refresh Token

// POST /auth/refresh
interface RefreshRequest {
  refresh_token: string
}

interface RefreshResponse {
  access_token: string
  refresh_token: string
  token_type: string
  expires_in: number
}

async function refreshAccessToken(): Promise<RefreshResponse> {
  const refreshToken = localStorage.getItem('refresh_token')
  
  const response = await apiClient.post<RefreshResponse>('/auth/refresh', {
    refresh_token: refreshToken
  })
  
  // Atualizar tokens
  localStorage.setItem('access_token', response.access_token)
  localStorage.setItem('refresh_token', response.refresh_token)
  
  return response
}

3. Logout

// POST /auth/logout
async function logout(): Promise<void> {
  try {
    await apiClient.post('/auth/logout')
  } finally {
    // Limpar dados locais
    localStorage.removeItem('access_token')
    localStorage.removeItem('refresh_token')
    localStorage.removeItem('user')
    
    // Redirecionar para login
    window.location.href = '/login'
  }
}

4. Get Current User

// GET /auth/me
interface UserInfo {
  id: string
  email: string
  name: string
  role: string
  is_active: boolean
  created_at: string
  last_login: string
}

async function getCurrentUser(): Promise<UserInfo> {
  return await apiClient.get<UserInfo>('/auth/me')
}

📡 Endpoints Principais

1. Chat API

Enviar Mensagem

// POST /api/v1/chat/message
interface ChatMessageRequest {
  message: string
  session_id?: string  // Opcional, será criado se não fornecido
  context?: Record<string, any>
}

interface ChatMessageResponse {
  response: string
  session_id: string
  message_id: string
  agent_used: string
  processing_time: number
  suggestions?: string[]
}

async function sendChatMessage(message: string, sessionId?: string): Promise<ChatMessageResponse> {
  return await apiClient.post<ChatMessageResponse>('/api/v1/chat/message', {
    message,
    session_id: sessionId
  })
}

Stream de Resposta (SSE)

// POST /api/v1/chat/stream
interface StreamToken {
  type: 'token' | 'error' | 'complete'
  content?: string
  error?: string
  metadata?: {
    agent: string
    processing_time?: number
  }
}

function streamChatMessage(message: string, sessionId?: string): EventSource {
  const token = localStorage.getItem('access_token')
  const url = `${API_BASE_URL}/api/v1/chat/stream?token=${token}`
  
  const eventSource = new EventSource(url, {
    headers: {
      'Content-Type': 'application/json',
    }
  })
  
  // Enviar mensagem inicial
  fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify({
      message,
      session_id: sessionId
    })
  })
  
  return eventSource
}

// Uso do SSE
const eventSource = streamChatMessage('Analise os contratos de 2024', sessionId)

eventSource.onmessage = (event) => {
  const data: StreamToken = JSON.parse(event.data)
  
  switch (data.type) {
    case 'token':
      // Adicionar token à resposta
      setResponse(prev => prev + data.content)
      break
    case 'complete':
      // Resposta completa
      eventSource.close()
      break
    case 'error':
      console.error('Stream error:', data.error)
      eventSource.close()
      break
  }
}

eventSource.onerror = (error) => {
  console.error('SSE Error:', error)
  eventSource.close()
}

Histórico de Chat

// GET /api/v1/chat/history/{session_id}/paginated
interface PaginatedChatHistory {
  messages: ChatMessage[]
  total: number
  page: number
  page_size: number
  has_next: boolean
  has_previous: boolean
}

interface ChatMessage {
  id: string
  role: 'user' | 'assistant'
  content: string
  timestamp: string
  agent_used?: string
  metadata?: Record<string, any>
}

async function getChatHistory(
  sessionId: string,
  page: number = 1,
  pageSize: number = 20
): Promise<PaginatedChatHistory> {
  return await apiClient.get<PaginatedChatHistory>(
    `/api/v1/chat/history/${sessionId}/paginated`,
    {
      params: { page, page_size: pageSize }
    }
  )
}

Sugestões Rápidas

// GET /api/v1/chat/suggestions
interface ChatSuggestion {
  id: string
  text: string
  category: 'investigation' | 'analysis' | 'report' | 'general'
  icon?: string
}

async function getChatSuggestions(context?: string): Promise<ChatSuggestion[]> {
  return await apiClient.get<ChatSuggestion[]>('/api/v1/chat/suggestions', {
    params: { context }
  })
}

2. Agentes de IA

Zumbi dos Palmares - Detecção de Anomalias

// POST /api/v1/agents/zumbi
interface ZumbiRequest {
  data: {
    contract_id?: string
    vendor_name?: string
    amount?: number
    date?: string
    description?: string
    [key: string]: any
  }
  analysis_type?: 'full' | 'quick' | 'pattern'
}

interface AnomalyResult {
  anomaly_score: number  // 0-1
  anomaly_type: string[]
  confidence: number
  severity: 'low' | 'medium' | 'high' | 'critical'
  details: {
    statistical_analysis: Record<string, any>
    pattern_analysis: Record<string, any>
    spectral_analysis?: Record<string, any>
  }
  recommendations: string[]
  visualizations?: {
    type: string
    data: any
  }[]
}

async function detectAnomalies(data: any): Promise<AnomalyResult> {
  return await apiClient.post<AnomalyResult>('/api/v1/agents/zumbi', {
    data,
    analysis_type: 'full'
  })
}

Status dos Agentes

// GET /api/v1/agents/status
interface AgentStatus {
  agent_id: string
  name: string
  status: 'idle' | 'processing' | 'error' | 'maintenance'
  health: {
    cpu_usage: number
    memory_usage: number
    response_time_ms: number
    success_rate: number
  }
  capabilities: string[]
  last_active: string
}

async function getAgentsStatus(): Promise<AgentStatus[]> {
  return await apiClient.get<AgentStatus[]>('/api/v1/agents/status')
}

3. Investigações

Iniciar Investigação

// POST /api/v1/investigations/start
interface StartInvestigationRequest {
  title: string
  description: string
  type: 'contract' | 'vendor' | 'pattern' | 'general'
  parameters: {
    date_range?: {
      start: string
      end: string
    }
    vendor_ids?: string[]
    contract_ids?: string[]
    amount_range?: {
      min: number
      max: number
    }
    keywords?: string[]
    [key: string]: any
  }
  agents?: string[]  // Agentes específicos para usar
  priority?: 'low' | 'medium' | 'high' | 'critical'
}

interface Investigation {
  id: string
  title: string
  status: 'pending' | 'running' | 'completed' | 'failed'
  progress: number  // 0-100
  created_at: string
  updated_at: string
  estimated_completion: string
  results?: InvestigationResults
}

async function startInvestigation(
  request: StartInvestigationRequest
): Promise<Investigation> {
  return await apiClient.post<Investigation>('/api/v1/investigations/start', request)
}

Acompanhar Investigação

// GET /api/v1/investigations/{id}
async function getInvestigation(id: string): Promise<Investigation> {
  return await apiClient.get<Investigation>(`/api/v1/investigations/${id}`)
}

// GET /api/v1/investigations/{id}/results
interface InvestigationResults {
  summary: string
  findings: Finding[]
  anomalies: AnomalyResult[]
  patterns: Pattern[]
  recommendations: string[]
  risk_score: number
  confidence: number
  visualizations: Visualization[]
  raw_data?: any
}

interface Finding {
  id: string
  type: string
  description: string
  severity: 'low' | 'medium' | 'high' | 'critical'
  evidence: any[]
  agent: string
}

async function getInvestigationResults(id: string): Promise<InvestigationResults> {
  return await apiClient.get<InvestigationResults>(`/api/v1/investigations/${id}/results`)
}

4. Análises

Análise de Padrões

// POST /api/v1/analysis/patterns
interface PatternAnalysisRequest {
  data: any[]
  analysis_config?: {
    min_support?: number
    min_confidence?: number
    algorithms?: ('apriori' | 'fpgrowth' | 'eclat')[]
  }
  time_range?: {
    start: string
    end: string
  }
}

interface PatternAnalysisResponse {
  patterns: Pattern[]
  statistics: {
    total_patterns: number
    avg_confidence: number
    processing_time: number
  }
  visualizations: Visualization[]
}

interface Pattern {
  id: string
  pattern: string[]
  support: number
  confidence: number
  lift: number
  occurrences: number
  examples: any[]
}

Análise de Tendências

// POST /api/v1/analysis/trends
interface TrendAnalysisRequest {
  metric: string
  data: {
    timestamp: string
    value: number
    metadata?: any
  }[]
  analysis_type: 'linear' | 'seasonal' | 'polynomial' | 'all'
  forecast_periods?: number
}

interface TrendAnalysisResponse {
  current_trend: 'increasing' | 'decreasing' | 'stable'
  trend_strength: number
  forecast: {
    timestamp: string
    value: number
    confidence_interval: {
      lower: number
      upper: number
    }
  }[]
  seasonality?: {
    period: string
    strength: number
  }
  change_points: {
    timestamp: string
    significance: number
  }[]
}

5. Relatórios

Gerar Relatório

// POST /api/v1/reports/generate
interface GenerateReportRequest {
  investigation_id?: string
  template: 'executive_summary' | 'detailed' | 'technical' | 'compliance'
  format: 'pdf' | 'html' | 'markdown' | 'docx'
  sections?: string[]
  include_visualizations?: boolean
  language?: 'pt-BR' | 'en-US'
}

interface Report {
  id: string
  title: string
  status: 'generating' | 'ready' | 'failed'
  format: string
  size_bytes: number
  download_url?: string
  preview_url?: string
  created_at: string
  expires_at: string
}

async function generateReport(request: GenerateReportRequest): Promise<Report> {
  return await apiClient.post<Report>('/api/v1/reports/generate', request)
}

Download Relatório

// GET /api/v1/reports/{id}/download
async function downloadReport(reportId: string): Promise<Blob> {
  const response = await apiClient.get(`/api/v1/reports/${reportId}/download`, {
    responseType: 'blob'
  })
  return response
}

// Exemplo de uso
const blob = await downloadReport(reportId)
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `relatorio-${reportId}.pdf`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)

🔄 WebSockets e Real-time

Configuração do WebSocket Client

// utils/websocket-client.ts
export class WebSocketClient {
  private ws: WebSocket | null = null
  private reconnectAttempts = 0
  private maxReconnectAttempts = 5
  private reconnectDelay = 1000
  private heartbeatInterval: NodeJS.Timeout | null = null
  private eventHandlers: Map<string, Set<Function>> = new Map()

  constructor(private baseUrl: string) {}

  connect(endpoint: string, token: string): Promise<void> {
    return new Promise((resolve, reject) => {
      const url = `${this.baseUrl}${endpoint}?token=${token}`
      
      try {
        this.ws = new WebSocket(url)
        
        this.ws.onopen = () => {
          console.log('WebSocket connected')
          this.reconnectAttempts = 0
          this.startHeartbeat()
          resolve()
        }
        
        this.ws.onmessage = (event) => {
          try {
            const data = JSON.parse(event.data)
            this.handleMessage(data)
          } catch (error) {
            console.error('Failed to parse WebSocket message:', error)
          }
        }
        
        this.ws.onerror = (error) => {
          console.error('WebSocket error:', error)
          reject(error)
        }
        
        this.ws.onclose = () => {
          console.log('WebSocket disconnected')
          this.stopHeartbeat()
          this.handleReconnect(endpoint, token)
        }
      } catch (error) {
        reject(error)
      }
    })
  }

  private handleMessage(data: any) {
    const { type, payload } = data
    
    const handlers = this.eventHandlers.get(type)
    if (handlers) {
      handlers.forEach(handler => handler(payload))
    }
    
    // Handler global
    const globalHandlers = this.eventHandlers.get('*')
    if (globalHandlers) {
      globalHandlers.forEach(handler => handler(data))
    }
  }

  on(event: string, handler: Function) {
    if (!this.eventHandlers.has(event)) {
      this.eventHandlers.set(event, new Set())
    }
    this.eventHandlers.get(event)!.add(handler)
  }

  off(event: string, handler: Function) {
    const handlers = this.eventHandlers.get(event)
    if (handlers) {
      handlers.delete(handler)
    }
  }

  send(type: string, payload: any) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, payload }))
    } else {
      console.error('WebSocket is not connected')
    }
  }

  private startHeartbeat() {
    this.heartbeatInterval = setInterval(() => {
      this.send('ping', { timestamp: Date.now() })
    }, 30000) // 30 segundos
  }

  private stopHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval)
      this.heartbeatInterval = null
    }
  }

  private handleReconnect(endpoint: string, token: string) {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++
      const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
      
      console.log(`Reconnecting in ${delay}ms... (attempt ${this.reconnectAttempts})`)
      
      setTimeout(() => {
        this.connect(endpoint, token)
      }, delay)
    } else {
      console.error('Max reconnection attempts reached')
    }
  }

  disconnect() {
    if (this.ws) {
      this.ws.close()
      this.ws = null
    }
    this.stopHeartbeat()
  }
}

WebSocket para Chat

// hooks/useWebSocketChat.ts
import { useEffect, useRef, useState } from 'react'
import { WebSocketClient } from '@/utils/websocket-client'

interface WebSocketMessage {
  type: 'message' | 'typing' | 'user_joined' | 'user_left' | 'error'
  payload: any
}

export function useWebSocketChat(sessionId: string) {
  const [messages, setMessages] = useState<any[]>([])
  const [isConnected, setIsConnected] = useState(false)
  const [typingUsers, setTypingUsers] = useState<string[]>([])
  const wsClient = useRef<WebSocketClient>()

  useEffect(() => {
    const token = localStorage.getItem('access_token')
    if (!token || !sessionId) return

    wsClient.current = new WebSocketClient(
      process.env.NEXT_PUBLIC_WS_URL || 'wss://neural-thinker-cidadao-ai-backend.hf.space'
    )

    // Conectar ao WebSocket
    wsClient.current.connect(`/api/v1/ws/chat/${sessionId}`, token)
      .then(() => {
        setIsConnected(true)
      })
      .catch((error) => {
        console.error('Failed to connect to WebSocket:', error)
      })

    // Configurar event handlers
    wsClient.current.on('message', (payload: any) => {
      setMessages(prev => [...prev, payload])
    })

    wsClient.current.on('typing', (payload: { user_id: string, is_typing: boolean }) => {
      setTypingUsers(prev => {
        if (payload.is_typing) {
          return [...prev, payload.user_id]
        } else {
          return prev.filter(id => id !== payload.user_id)
        }
      })
    })

    wsClient.current.on('error', (payload: any) => {
      console.error('WebSocket error:', payload)
    })

    // Cleanup
    return () => {
      if (wsClient.current) {
        wsClient.current.disconnect()
      }
    }
  }, [sessionId])

  const sendMessage = (message: string) => {
    if (wsClient.current && isConnected) {
      wsClient.current.send('message', {
        content: message,
        timestamp: new Date().toISOString()
      })
    }
  }

  const sendTypingIndicator = (isTyping: boolean) => {
    if (wsClient.current && isConnected) {
      wsClient.current.send('typing', { is_typing: isTyping })
    }
  }

  return {
    messages,
    isConnected,
    typingUsers,
    sendMessage,
    sendTypingIndicator
  }
}

WebSocket para Investigações

// hooks/useInvestigationWebSocket.ts
export function useInvestigationWebSocket(investigationId: string) {
  const [status, setStatus] = useState<string>('pending')
  const [progress, setProgress] = useState(0)
  const [findings, setFindings] = useState<any[]>([])
  const [logs, setLogs] = useState<string[]>([])
  
  useEffect(() => {
    if (!investigationId) return
    
    const token = localStorage.getItem('access_token')
    const wsClient = new WebSocketClient(process.env.NEXT_PUBLIC_WS_URL!)
    
    wsClient.connect(`/api/v1/ws/investigations/${investigationId}`, token!)
      .then(() => {
        console.log('Connected to investigation WebSocket')
      })
    
    wsClient.on('status_update', (payload: { status: string, progress: number }) => {
      setStatus(payload.status)
      setProgress(payload.progress)
    })
    
    wsClient.on('finding', (payload: any) => {
      setFindings(prev => [...prev, payload])
    })
    
    wsClient.on('log', (payload: { message: string, level: string }) => {
      setLogs(prev => [...prev, `[${payload.level}] ${payload.message}`])
    })
    
    wsClient.on('complete', (payload: { results: any }) => {
      setStatus('completed')
      setProgress(100)
      // Processar resultados finais
    })
    
    return () => {
      wsClient.disconnect()
    }
  }, [investigationId])
  
  return { status, progress, findings, logs }
}

💻 Exemplos de Implementação

1. Componente de Chat Completo

// components/Chat/ChatInterface.tsx
import React, { useState, useRef, useEffect } from 'react'
import { useWebSocketChat } from '@/hooks/useWebSocketChat'
import { apiClient } from '@/utils/api-client'

interface ChatInterfaceProps {
  sessionId?: string
}

export function ChatInterface({ sessionId: initialSessionId }: ChatInterfaceProps) {
  const [sessionId, setSessionId] = useState(initialSessionId || '')
  const [messages, setMessages] = useState<any[]>([])
  const [input, setInput] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [isStreaming, setIsStreaming] = useState(false)
  const messagesEndRef = useRef<HTMLDivElement>(null)
  
  const { 
    messages: wsMessages,
    isConnected,
    sendMessage: wsSendMessage,
    sendTypingIndicator
  } = useWebSocketChat(sessionId)
  
  // Carregar histórico ao montar
  useEffect(() => {
    if (sessionId) {
      loadChatHistory()
    }
  }, [sessionId])
  
  // Scroll automático
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages, wsMessages])
  
  const loadChatHistory = async () => {
    try {
      const history = await apiClient.get(
        `/api/v1/chat/history/${sessionId}/paginated`
      )
      setMessages(history.messages)
    } catch (error) {
      console.error('Failed to load chat history:', error)
    }
  }
  
  const sendMessage = async () => {
    if (!input.trim()) return
    
    const userMessage = {
      role: 'user',
      content: input,
      timestamp: new Date().toISOString()
    }
    
    setMessages(prev => [...prev, userMessage])
    setInput('')
    setIsLoading(true)
    
    try {
      // Se WebSocket conectado, usar WebSocket
      if (isConnected) {
        wsSendMessage(input)
      } else {
        // Senão, usar API REST
        const response = await apiClient.post('/api/v1/chat/message', {
          message: input,
          session_id: sessionId
        })
        
        if (!sessionId) {
          setSessionId(response.session_id)
        }
        
        setMessages(prev => [...prev, {
          role: 'assistant',
          content: response.response,
          agent_used: response.agent_used,
          timestamp: new Date().toISOString()
        }])
      }
    } catch (error) {
      console.error('Failed to send message:', error)
      // Mostrar erro ao usuário
    } finally {
      setIsLoading(false)
    }
  }
  
  const streamMessage = () => {
    if (!input.trim()) return
    
    const userMessage = {
      role: 'user',
      content: input,
      timestamp: new Date().toISOString()
    }
    
    setMessages(prev => [...prev, userMessage])
    setInput('')
    setIsStreaming(true)
    
    let assistantMessage = {
      role: 'assistant',
      content: '',
      timestamp: new Date().toISOString(),
      isStreaming: true
    }
    
    setMessages(prev => [...prev, assistantMessage])
    
    // Usar SSE para streaming
    const eventSource = streamChatMessage(input, sessionId)
    
    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data)
      
      if (data.type === 'token') {
        assistantMessage.content += data.content
        setMessages(prev => {
          const newMessages = [...prev]
          newMessages[newMessages.length - 1] = { ...assistantMessage }
          return newMessages
        })
      } else if (data.type === 'complete') {
        assistantMessage.isStreaming = false
        assistantMessage.agent_used = data.metadata?.agent
        setMessages(prev => {
          const newMessages = [...prev]
          newMessages[newMessages.length - 1] = { ...assistantMessage }
          return newMessages
        })
        setIsStreaming(false)
        eventSource.close()
      }
    }
    
    eventSource.onerror = () => {
      setIsStreaming(false)
      eventSource.close()
    }
  }
  
  return (
    <div className="chat-container">
      <div className="messages-container">
        {messages.map((message, index) => (
          <MessageComponent key={index} message={message} />
        ))}
        {isLoading && <LoadingIndicator />}
        <div ref={messagesEndRef} />
      </div>
      
      <div className="input-container">
        <input
          type="text"
          value={input}
          onChange={(e) => {
            setInput(e.target.value)
            sendTypingIndicator(true)
          }}
          onBlur={() => sendTypingIndicator(false)}
          onKeyPress={(e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
              e.preventDefault()
              sendMessage()
            }
          }}
          placeholder="Digite sua mensagem..."
          disabled={isLoading || isStreaming}
        />
        
        <button 
          onClick={sendMessage}
          disabled={isLoading || isStreaming || !input.trim()}
        >
          Enviar
        </button>
        
        <button 
          onClick={streamMessage}
          disabled={isLoading || isStreaming || !input.trim()}
        >
          Stream
        </button>
      </div>
      
      {isConnected && (
        <div className="connection-status">
          <span className="status-dot online" />
          Conectado
        </div>
      )}
    </div>
  )
}

2. Dashboard de Investigações

// components/Investigations/InvestigationsDashboard.tsx
import React, { useState, useEffect } from 'react'
import { useInvestigationWebSocket } from '@/hooks/useInvestigationWebSocket'
import { apiClient } from '@/utils/api-client'

export function InvestigationsDashboard() {
  const [investigations, setInvestigations] = useState<any[]>([])
  const [selectedInvestigation, setSelectedInvestigation] = useState<string>('')
  const [isCreating, setIsCreating] = useState(false)
  
  // Carregar investigações
  useEffect(() => {
    loadInvestigations()
  }, [])
  
  const loadInvestigations = async () => {
    try {
      const data = await apiClient.get('/api/v1/investigations/history')
      setInvestigations(data)
    } catch (error) {
      console.error('Failed to load investigations:', error)
    }
  }
  
  const createInvestigation = async (data: any) => {
    setIsCreating(true)
    try {
      const investigation = await apiClient.post('/api/v1/investigations/start', {
        title: data.title,
        description: data.description,
        type: data.type,
        parameters: data.parameters,
        priority: 'high'
      })
      
      setInvestigations(prev => [investigation, ...prev])
      setSelectedInvestigation(investigation.id)
    } catch (error) {
      console.error('Failed to create investigation:', error)
    } finally {
      setIsCreating(false)
    }
  }
  
  return (
    <div className="investigations-dashboard">
      <div className="investigations-list">
        <h2>Investigações</h2>
        <button onClick={() => setIsCreating(true)}>
          Nova Investigação
        </button>
        
        {investigations.map(inv => (
          <InvestigationCard
            key={inv.id}
            investigation={inv}
            isSelected={inv.id === selectedInvestigation}
            onClick={() => setSelectedInvestigation(inv.id)}
          />
        ))}
      </div>
      
      <div className="investigation-detail">
        {selectedInvestigation && (
          <InvestigationDetail investigationId={selectedInvestigation} />
        )}
      </div>
      
      {isCreating && (
        <CreateInvestigationModal
          onClose={() => setIsCreating(false)}
          onCreate={createInvestigation}
        />
      )}
    </div>
  )
}

// Componente de detalhe com WebSocket
function InvestigationDetail({ investigationId }: { investigationId: string }) {
  const { status, progress, findings, logs } = useInvestigationWebSocket(investigationId)
  const [results, setResults] = useState<any>(null)
  
  useEffect(() => {
    if (status === 'completed') {
      loadResults()
    }
  }, [status])
  
  const loadResults = async () => {
    try {
      const data = await apiClient.get(
        `/api/v1/investigations/${investigationId}/results`
      )
      setResults(data)
    } catch (error) {
      console.error('Failed to load results:', error)
    }
  }
  
  return (
    <div className="investigation-detail-container">
      <div className="status-header">
        <h3>Status: {status}</h3>
        <ProgressBar value={progress} />
      </div>
      
      {status === 'running' && (
        <>
          <div className="findings-section">
            <h4>Descobertas ({findings.length})</h4>
            {findings.map((finding, i) => (
              <FindingCard key={i} finding={finding} />
            ))}
          </div>
          
          <div className="logs-section">
            <h4>Logs</h4>
            <div className="logs-container">
              {logs.map((log, i) => (
                <div key={i} className="log-entry">{log}</div>
              ))}
            </div>
          </div>
        </>
      )}
      
      {status === 'completed' && results && (
        <InvestigationResults results={results} />
      )}
    </div>
  )
}

3. Hook para Análise de Dados

// hooks/useDataAnalysis.ts
import { useState } from 'react'
import { apiClient } from '@/utils/api-client'

export function useDataAnalysis() {
  const [isAnalyzing, setIsAnalyzing] = useState(false)
  const [results, setResults] = useState<any>(null)
  const [error, setError] = useState<string | null>(null)
  
  const analyzePatterns = async (data: any[], config?: any) => {
    setIsAnalyzing(true)
    setError(null)
    
    try {
      const response = await apiClient.post('/api/v1/analysis/patterns', {
        data,
        analysis_config: config
      })
      
      setResults(response)
      return response
    } catch (err: any) {
      setError(err.message)
      throw err
    } finally {
      setIsAnalyzing(false)
    }
  }
  
  const analyzeTrends = async (metric: string, data: any[], options?: any) => {
    setIsAnalyzing(true)
    setError(null)
    
    try {
      const response = await apiClient.post('/api/v1/analysis/trends', {
        metric,
        data,
        ...options
      })
      
      setResults(response)
      return response
    } catch (err: any) {
      setError(err.message)
      throw err
    } finally {
      setIsAnalyzing(false)
    }
  }
  
  const detectAnomalies = async (data: any) => {
    setIsAnalyzing(true)
    setError(null)
    
    try {
      const response = await apiClient.post('/api/v1/agents/zumbi', {
        data,
        analysis_type: 'full'
      })
      
      setResults(response)
      return response
    } catch (err: any) {
      setError(err.message)
      throw err
    } finally {
      setIsAnalyzing(false)
    }
  }
  
  return {
    isAnalyzing,
    results,
    error,
    analyzePatterns,
    analyzeTrends,
    detectAnomalies
  }
}

📝 TypeScript Interfaces

Interfaces Principais

// types/api.ts

// Base Types
export interface ApiResponse<T> {
  data: T
  status: number
  message?: string
  error?: string
}

export interface PaginatedResponse<T> {
  items: T[]
  total: number
  page: number
  page_size: number
  has_next: boolean
  has_previous: boolean
}

// User & Auth
export interface User {
  id: string
  email: string
  name: string
  role: 'admin' | 'user' | 'viewer'
  is_active: boolean
  created_at: string
  last_login?: string
}

export interface AuthTokens {
  access_token: string
  refresh_token: string
  token_type: string
  expires_in: number
}

// Chat Types
export interface ChatMessage {
  id: string
  role: 'user' | 'assistant' | 'system'
  content: string
  timestamp: string
  session_id: string
  agent_used?: string
  metadata?: Record<string, any>
  attachments?: Attachment[]
}

export interface ChatSession {
  id: string
  user_id: string
  created_at: string
  updated_at: string
  message_count: number
  is_active: boolean
  context?: Record<string, any>
}

// Agent Types
export interface Agent {
  id: string
  name: string
  type: string
  description: string
  capabilities: string[]
  status: AgentStatus
  performance_metrics: AgentMetrics
}

export type AgentStatus = 'idle' | 'processing' | 'error' | 'maintenance'

export interface AgentMetrics {
  avg_response_time: number
  success_rate: number
  total_requests: number
  last_24h_requests: number
}

// Investigation Types
export interface Investigation {
  id: string
  title: string
  description: string
  type: InvestigationType
  status: InvestigationStatus
  priority: Priority
  progress: number
  user_id: string
  created_at: string
  updated_at: string
  started_at?: string
  completed_at?: string
  estimated_completion?: string
  results?: InvestigationResults
  error?: string
}

export type InvestigationType = 'contract' | 'vendor' | 'pattern' | 'general'
export type InvestigationStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
export type Priority = 'low' | 'medium' | 'high' | 'critical'

export interface InvestigationResults {
  summary: string
  findings: Finding[]
  anomalies: Anomaly[]
  patterns: Pattern[]
  recommendations: string[]
  risk_score: number
  confidence: number
  processing_time: number
  agents_used: string[]
  visualizations?: Visualization[]
}

export interface Finding {
  id: string
  type: string
  title: string
  description: string
  severity: Severity
  confidence: number
  evidence: Evidence[]
  detected_by: string
  detected_at: string
}

export type Severity = 'low' | 'medium' | 'high' | 'critical'

export interface Evidence {
  type: string
  source: string
  data: any
  relevance: number
}

// Analysis Types
export interface Anomaly {
  id: string
  type: string
  description: string
  anomaly_score: number
  severity: Severity
  affected_items: any[]
  detection_method: string
  statistical_significance: number
  context: Record<string, any>
}

export interface Pattern {
  id: string
  pattern: string[]
  type: string
  support: number
  confidence: number
  lift: number
  occurrences: number
  first_seen: string
  last_seen: string
  examples: any[]
}

export interface Trend {
  metric: string
  direction: 'increasing' | 'decreasing' | 'stable'
  strength: number
  change_rate: number
  forecast: ForecastPoint[]
  seasonality?: Seasonality
  change_points: ChangePoint[]
}

export interface ForecastPoint {
  timestamp: string
  value: number
  lower_bound: number
  upper_bound: number
  confidence: number
}

export interface Seasonality {
  period: string
  strength: number
  pattern: number[]
}

export interface ChangePoint {
  timestamp: string
  significance: number
  direction: 'up' | 'down'
}

// Report Types
export interface Report {
  id: string
  title: string
  type: ReportType
  format: ReportFormat
  status: ReportStatus
  size_bytes: number
  pages?: number
  download_url?: string
  preview_url?: string
  created_at: string
  expires_at: string
  metadata: Record<string, any>
}

export type ReportType = 'executive_summary' | 'detailed' | 'technical' | 'compliance'
export type ReportFormat = 'pdf' | 'html' | 'markdown' | 'docx'
export type ReportStatus = 'generating' | 'ready' | 'failed' | 'expired'

// Visualization Types
export interface Visualization {
  id: string
  type: VisualizationType
  title: string
  description?: string
  data: any
  config: VisualizationConfig
  interactive: boolean
}

export type VisualizationType = 
  | 'line_chart' 
  | 'bar_chart' 
  | 'pie_chart' 
  | 'scatter_plot'
  | 'heatmap'
  | 'network_graph'
  | 'timeline'
  | 'geographic_map'

export interface VisualizationConfig {
  width?: number
  height?: number
  colors?: string[]
  theme?: 'light' | 'dark'
  [key: string]: any
}

// Notification Types
export interface Notification {
  id: string
  type: NotificationType
  title: string
  message: string
  severity: Severity
  read: boolean
  created_at: string
  data?: any
  action_url?: string
}

export type NotificationType = 
  | 'anomaly_detected'
  | 'investigation_complete'
  | 'report_ready'
  | 'system_alert'
  | 'agent_update'

// WebSocket Event Types
export interface WebSocketEvent<T = any> {
  type: string
  payload: T
  timestamp: string
  correlation_id?: string
}

export interface ChatWebSocketEvent extends WebSocketEvent {
  type: 'message' | 'typing' | 'user_joined' | 'user_left' | 'error'
}

export interface InvestigationWebSocketEvent extends WebSocketEvent {
  type: 'status_update' | 'progress' | 'finding' | 'log' | 'complete' | 'error'
}

// Error Types
export interface ApiError {
  error: string
  message: string
  status_code: number
  details?: any
  timestamp: string
  request_id?: string
}

export interface ValidationError extends ApiError {
  validation_errors: {
    field: string
    message: string
    code: string
  }[]
}

// Rate Limit Types
export interface RateLimitInfo {
  limit: number
  remaining: number
  reset: number
  tier: 'free' | 'basic' | 'premium' | 'enterprise'
}

// File Types
export interface Attachment {
  id: string
  filename: string
  content_type: string
  size_bytes: number
  url: string
  thumbnail_url?: string
  uploaded_at: string
}

🚨 Tratamento de Erros

Padrões de Erro da API

// utils/error-handler.ts
export class ApiErrorHandler {
  static handle(error: any): never {
    if (error.response) {
      // Erro da API
      const { status, data } = error.response
      
      switch (status) {
        case 400:
          throw new BadRequestError(data.message || 'Requisição inválida', data)
        
        case 401:
          throw new UnauthorizedError(data.message || 'Não autorizado', data)
        
        case 403:
          throw new ForbiddenError(data.message || 'Acesso negado', data)
        
        case 404:
          throw new NotFoundError(data.message || 'Recurso não encontrado', data)
        
        case 422:
          throw new ValidationError(data.message || 'Erro de validação', data.validation_errors)
        
        case 429:
          throw new RateLimitError(
            data.message || 'Limite de requisições excedido',
            data.retry_after
          )
        
        case 500:
          throw new ServerError(data.message || 'Erro interno do servidor', data)
        
        default:
          throw new ApiError(data.message || 'Erro desconhecido', status, data)
      }
    } else if (error.request) {
      // Erro de rede
      throw new NetworkError('Erro de conexão com o servidor')
    } else {
      // Erro desconhecido
      throw new UnknownError(error.message || 'Erro desconhecido')
    }
  }
}

// Classes de erro customizadas
export class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public details?: any
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

export class BadRequestError extends ApiError {
  constructor(message: string, details?: any) {
    super(message, 400, details)
    this.name = 'BadRequestError'
  }
}

export class UnauthorizedError extends ApiError {
  constructor(message: string, details?: any) {
    super(message, 401, details)
    this.name = 'UnauthorizedError'
  }
}

export class ValidationError extends ApiError {
  constructor(message: string, public validationErrors: any[]) {
    super(message, 422, validationErrors)
    this.name = 'ValidationError'
  }
}

export class RateLimitError extends ApiError {
  constructor(message: string, public retryAfter: number) {
    super(message, 429, { retry_after: retryAfter })
    this.name = 'RateLimitError'
  }
}

Hook para Tratamento de Erros

// hooks/useApiError.ts
import { useState, useCallback } from 'react'
import { ApiError, ValidationError, RateLimitError } from '@/utils/error-handler'

export function useApiError() {
  const [error, setError] = useState<ApiError | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  
  const execute = useCallback(async <T>(
    apiCall: () => Promise<T>,
    options?: {
      onSuccess?: (data: T) => void
      onError?: (error: ApiError) => void
      showToast?: boolean
    }
  ): Promise<T | null> => {
    setIsLoading(true)
    setError(null)
    
    try {
      const result = await apiCall()
      options?.onSuccess?.(result)
      return result
    } catch (err: any) {
      const apiError = err instanceof ApiError ? err : new ApiError(
        err.message || 'Erro desconhecido',
        500
      )
      
      setError(apiError)
      options?.onError?.(apiError)
      
      if (options?.showToast) {
        showErrorToast(apiError)
      }
      
      return null
    } finally {
      setIsLoading(false)
    }
  }, [])
  
  const clearError = useCallback(() => {
    setError(null)
  }, [])
  
  return {
    error,
    isLoading,
    execute,
    clearError
  }
}

// Função auxiliar para mostrar toast
function showErrorToast(error: ApiError) {
  let message = error.message
  
  if (error instanceof ValidationError) {
    message = error.validationErrors
      .map(err => `${err.field}: ${err.message}`)
      .join('\n')
  } else if (error instanceof RateLimitError) {
    message = `${error.message}. Tente novamente em ${error.retryAfter}s`
  }
  
  // Implementar toast notification
  console.error('Toast:', message)
}

⚡ Rate Limiting

Headers de Rate Limit

interface RateLimitHeaders {
  'X-RateLimit-Limit': string      // Limite total
  'X-RateLimit-Remaining': string  // Requisições restantes
  'X-RateLimit-Reset': string       // Timestamp do reset
  'X-RateLimit-Tier': string        // Tier atual
}

Implementação de Rate Limit Handler

// utils/rate-limit-handler.ts
export class RateLimitHandler {
  private static instance: RateLimitHandler
  private limitInfo: Map<string, RateLimitInfo> = new Map()
  
  static getInstance(): RateLimitHandler {
    if (!RateLimitHandler.instance) {
      RateLimitHandler.instance = new RateLimitHandler()
    }
    return RateLimitHandler.instance
  }
  
  updateFromHeaders(endpoint: string, headers: any) {
    const limit = parseInt(headers['x-ratelimit-limit'] || '0')
    const remaining = parseInt(headers['x-ratelimit-remaining'] || '0')
    const reset = parseInt(headers['x-ratelimit-reset'] || '0')
    const tier = headers['x-ratelimit-tier'] || 'free'
    
    this.limitInfo.set(endpoint, {
      limit,
      remaining,
      reset,
      tier
    })
  }
  
  getRemainingRequests(endpoint: string): number {
    const info = this.limitInfo.get(endpoint)
    return info?.remaining ?? -1
  }
  
  getResetTime(endpoint: string): Date | null {
    const info = this.limitInfo.get(endpoint)
    return info ? new Date(info.reset * 1000) : null
  }
  
  shouldThrottle(endpoint: string, threshold: number = 10): boolean {
    const remaining = this.getRemainingRequests(endpoint)
    return remaining !== -1 && remaining < threshold
  }
  
  getWaitTime(endpoint: string): number {
    const resetTime = this.getResetTime(endpoint)
    if (!resetTime) return 0
    
    const now = new Date()
    const waitMs = resetTime.getTime() - now.getTime()
    return Math.max(0, Math.ceil(waitMs / 1000))
  }
}

// Hook para monitorar rate limit
export function useRateLimit(endpoint: string) {
  const [limitInfo, setLimitInfo] = useState<RateLimitInfo | null>(null)
  const handler = RateLimitHandler.getInstance()
  
  useEffect(() => {
    // Atualizar a cada segundo
    const interval = setInterval(() => {
      const info = handler.limitInfo.get(endpoint)
      if (info) {
        setLimitInfo({ ...info })
      }
    }, 1000)
    
    return () => clearInterval(interval)
  }, [endpoint])
  
  return {
    limit: limitInfo?.limit ?? 0,
    remaining: limitInfo?.remaining ?? 0,
    reset: limitInfo ? new Date(limitInfo.reset * 1000) : null,
    tier: limitInfo?.tier ?? 'free',
    shouldThrottle: handler.shouldThrottle(endpoint),
    waitTime: handler.getWaitTime(endpoint)
  }
}

🎯 Boas Práticas

1. Gerenciamento de Estado

// store/chat-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface ChatStore {
  sessions: Map<string, ChatSession>
  activeSessionId: string | null
  messages: Map<string, ChatMessage[]>
  
  // Actions
  setActiveSession: (sessionId: string) => void
  addMessage: (sessionId: string, message: ChatMessage) => void
  loadSession: (session: ChatSession) => void
  clearSession: (sessionId: string) => void
}

export const useChatStore = create<ChatStore>()(
  persist(
    (set, get) => ({
      sessions: new Map(),
      activeSessionId: null,
      messages: new Map(),
      
      setActiveSession: (sessionId) => set({ activeSessionId: sessionId }),
      
      addMessage: (sessionId, message) => set((state) => {
        const messages = state.messages.get(sessionId) || []
        state.messages.set(sessionId, [...messages, message])
        return { messages: new Map(state.messages) }
      }),
      
      loadSession: (session) => set((state) => {
        state.sessions.set(session.id, session)
        return { sessions: new Map(state.sessions) }
      }),
      
      clearSession: (sessionId) => set((state) => {
        state.sessions.delete(sessionId)
        state.messages.delete(sessionId)
        return {
          sessions: new Map(state.sessions),
          messages: new Map(state.messages)
        }
      })
    }),
    {
      name: 'cidadao-ai-chat',
      partialize: (state) => ({
        activeSessionId: state.activeSessionId
      })
    }
  )
)

2. Otimização de Performance

// components/OptimizedChat.tsx
import React, { memo, useMemo, useCallback } from 'react'
import { FixedSizeList as List } from 'react-window'
import AutoSizer from 'react-virtualized-auto-sizer'

// Memoizar componentes de mensagem
const MessageItem = memo(({ message }: { message: ChatMessage }) => {
  return (
    <div className={`message ${message.role}`}>
      <div className="content">{message.content}</div>
      <div className="metadata">
        {message.timestamp} • {message.agent_used}
      </div>
    </div>
  )
})

// Lista virtualizada para muitas mensagens
export function OptimizedMessageList({ messages }: { messages: ChatMessage[] }) {
  const Row = useCallback(({ index, style }: any) => (
    <div style={style}>
      <MessageItem message={messages[index]} />
    </div>
  ), [messages])
  
  return (
    <AutoSizer>
      {({ height, width }) => (
        <List
          height={height}
          itemCount={messages.length}
          itemSize={100} // Altura estimada de cada mensagem
          width={width}
        >
          {Row}
        </List>
      )}
    </AutoSizer>
  )
}

3. Cache e Persistência

// utils/cache-manager.ts
export class CacheManager {
  private static instance: CacheManager
  private cache: Map<string, { data: any, expires: number }> = new Map()
  
  static getInstance(): CacheManager {
    if (!CacheManager.instance) {
      CacheManager.instance = new CacheManager()
    }
    return CacheManager.instance
  }
  
  set(key: string, data: any, ttl: number = 300000) { // 5 minutos padrão
    const expires = Date.now() + ttl
    this.cache.set(key, { data, expires })
  }
  
  get<T>(key: string): T | null {
    const item = this.cache.get(key)
    if (!item) return null
    
    if (Date.now() > item.expires) {
      this.cache.delete(key)
      return null
    }
    
    return item.data as T
  }
  
  invalidate(pattern: string) {
    const keys = Array.from(this.cache.keys())
    keys.forEach(key => {
      if (key.includes(pattern)) {
        this.cache.delete(key)
      }
    })
  }
  
  clear() {
    this.cache.clear()
  }
}

// Hook com cache
export function useCachedApi<T>(
  key: string,
  fetcher: () => Promise<T>,
  options?: {
    ttl?: number
    refetchOnMount?: boolean
    refetchInterval?: number
  }
) {
  const [data, setData] = useState<T | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)
  const cache = CacheManager.getInstance()
  
  const fetchData = useCallback(async () => {
    // Verificar cache primeiro
    const cached = cache.get<T>(key)
    if (cached) {
      setData(cached)
      setIsLoading(false)
      return cached
    }
    
    try {
      setIsLoading(true)
      const result = await fetcher()
      cache.set(key, result, options?.ttl)
      setData(result)
      return result
    } catch (err: any) {
      setError(err)
      throw err
    } finally {
      setIsLoading(false)
    }
  }, [key, fetcher, options?.ttl])
  
  useEffect(() => {
    if (options?.refetchOnMount !== false) {
      fetchData()
    }
    
    if (options?.refetchInterval) {
      const interval = setInterval(fetchData, options.refetchInterval)
      return () => clearInterval(interval)
    }
  }, [])
  
  return { data, isLoading, error, refetch: fetchData }
}

4. Monitoramento e Analytics

// utils/analytics.ts
export class Analytics {
  static trackEvent(event: string, properties?: any) {
    // Implementar tracking
    console.log('Track event:', event, properties)
  }
  
  static trackApiCall(endpoint: string, duration: number, status: number) {
    this.trackEvent('api_call', {
      endpoint,
      duration,
      status,
      timestamp: new Date().toISOString()
    })
  }
  
  static trackError(error: Error, context?: any) {
    this.trackEvent('error', {
      message: error.message,
      stack: error.stack,
      context,
      timestamp: new Date().toISOString()
    })
  }
  
  static trackPerformance(metric: string, value: number) {
    this.trackEvent('performance', {
      metric,
      value,
      timestamp: new Date().toISOString()
    })
  }
}

// Interceptor para analytics
axios.interceptors.request.use((config) => {
  config.metadata = { startTime: Date.now() }
  return config
})

axios.interceptors.response.use(
  (response) => {
    const duration = Date.now() - response.config.metadata.startTime
    Analytics.trackApiCall(
      response.config.url!,
      duration,
      response.status
    )
    return response
  },
  (error) => {
    if (error.response) {
      const duration = Date.now() - error.config.metadata.startTime
      Analytics.trackApiCall(
        error.config.url,
        duration,
        error.response.status
      )
    }
    Analytics.trackError(error)
    return Promise.reject(error)
  }
)

5. Segurança

// utils/security.ts
export class Security {
  // Sanitizar input do usuário
  static sanitizeInput(input: string): string {
    return input
      .replace(/[<>]/g, '') // Remover tags HTML básicas
      .trim()
      .slice(0, 5000) // Limitar tamanho
  }
  
  // Validar URLs
  static isValidUrl(url: string): boolean {
    try {
      const parsed = new URL(url)
      return ['http:', 'https:'].includes(parsed.protocol)
    } catch {
      return false
    }
  }
  
  // Storage seguro
  static secureStorage = {
    setItem(key: string, value: any) {
      try {
        const encrypted = btoa(JSON.stringify(value))
        localStorage.setItem(key, encrypted)
      } catch (error) {
        console.error('Failed to save to storage:', error)
      }
    },
    
    getItem<T>(key: string): T | null {
      try {
        const encrypted = localStorage.getItem(key)
        if (!encrypted) return null
        return JSON.parse(atob(encrypted)) as T
      } catch {
        return null
      }
    },
    
    removeItem(key: string) {
      localStorage.removeItem(key)
    }
  }
}

📚 Recursos Adicionais

Links Úteis

Variáveis de Ambiente Recomendadas

# .env.local
NEXT_PUBLIC_API_URL=https://neural-thinker-cidadao-ai-backend.hf.space
NEXT_PUBLIC_WS_URL=wss://neural-thinker-cidadao-ai-backend.hf.space
NEXT_PUBLIC_APP_NAME=Cidadão.AI
NEXT_PUBLIC_APP_VERSION=1.0.0
NEXT_PUBLIC_ENABLE_ANALYTICS=true
NEXT_PUBLIC_SENTRY_DSN=your-sentry-dsn

Scripts Úteis para package.json

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "type-check": "tsc --noEmit",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "analyze": "ANALYZE=true next build",
    "generate-types": "openapi-typescript https://neural-thinker-cidadao-ai-backend.hf.space/openapi.json --output ./src/types/api-generated.ts"
  }
}

🤝 Suporte e Contato

Para dúvidas sobre a integração:

  1. Consulte a documentação interativa em /docs
  2. Verifique os logs de erro retornados pela API
  3. Use o endpoint /health para verificar status dos serviços
  4. Monitore rate limits através dos headers de resposta

Este guia será atualizado conforme novas funcionalidades forem adicionadas ao backend. Mantenha-se atualizado com as versões da API através do endpoint /api/v1/info.