# Guia de Implementação Frontend — Chat IA (RAG) ## Visão Geral Este documento descreve como integrar o frontend com a API de chat IA do Decompile-AI. O fluxo consiste em: 1. Após upload + análise estática de um binário, embeddings são gerados automaticamente 2. O usuário cria uma sessão de chat vinculada ao binário 3. Mensagens são enviadas e a resposta da LLM é recebida via **Server-Sent Events (SSE)** 4. O backend faz RAG automaticamente: busca contexto relevante no binário e injeta no prompt ## Stack Recomendada (Frontend) | Tecnologia | Propósito | |---|---| | **EventSource** ou **fetch + ReadableStream** | Consumir SSE stream | | **React** (ou framework similar) | UI components | | **AbortController** | Cancelar stream em andamento | | **@microsoft/fetch-event-source** | Lib opcional para SSE com POST + cancelamento | --- ## 1. Endpoints da API ### 1.1 Criar Sessão de Chat ``` POST /binaries/{binaryId}/chat/sessions?projectId={projectId}&model={model} ``` **Query Params**: | Parâmetro | Tipo | Obrigatório | Descrição | |---|---|---|---| | `projectId` | UUID | SIM | ID do projeto pai | | `model` | String | Não | Modelo LLM (`gemma4:12b` padrão) | **Response** (`201 Created`): ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "binaryId": "660e8400-e29b-41d4-a716-446655440001", "projectId": "770e8400-e29b-41d4-a716-446655440002", "title": "malware.exe — Chat", "chatModel": "gemma4:12b", "messageCount": 0, "createdAt": "2026-06-08T15:30:00Z", "updatedAt": "2026-06-08T15:30:00Z" } ``` **Erros**: | Código | Significado | |---|---| | `404` | Binário/projeto não encontrado | | `400` | Modelo indisponível (Ollama não está rodando) | | `400` | `projectId` ausente | ### 1.2 Listar Sessões do Binário ``` GET /binaries/{binaryId}/chat/sessions ``` **Response** (`200 OK`): ```json [ { "id": "550e8400-...", "binaryId": "660e8400-...", "projectId": "770e8400-...", "title": "malware.exe — Chat", "chatModel": "gemma4:12b", "messageCount": 5, "createdAt": "2026-06-08T15:30:00Z", "updatedAt": "2026-06-08T16:00:00Z" } ] ``` Ordenado por `updatedAt` decrescente (sessão mais recente primeiro). ### 1.3 Ver Detalhes da Sessão ``` GET /chat/sessions/{sessionId} ``` Mesmo formato de resposta que o item 1.1. ### 1.4 Deletar Sessão ``` DELETE /chat/sessions/{sessionId} ``` **Response**: `204 No Content`. > Deletar a sessão remove **todas** as mensagens associadas (cascade). ### 1.5 Histórico de Mensagens ``` GET /chat/sessions/{sessionId}/messages ``` **Response** (`200 OK`): ```json [ { "id": "aa0e8400-...", "sessionId": "550e8400-...", "role": "USER", "content": "Quais funções lidam com rede?", "tokenCount": 12, "createdAt": "2026-06-08T15:30:30Z" }, { "id": "bb0e8400-...", "sessionId": "550e8400-...", "role": "ASSISTANT", "content": "O binário importa funções de rede da WS2_32.dll...", "tokenCount": 85, "createdAt": "2026-06-08T15:30:35Z" } ] ``` Ordenado por `createdAt` crescente (mais antigo primeiro). ### 1.6 Enviar Mensagem (SSE Streaming) ``` POST /chat/sessions/{sessionId}/messages Content-Type: application/json Accept: text/event-stream ``` **Request Body**: ```json { "content": "Quais funções nesse binário lidam com criptografia?" } ``` | Campo | Tipo | Validação | |---|---|---| | `content` | String | 1–4000 caracteres, obrigatório | **Response**: `200 OK` com `Content-Type: text/event-stream` --- ## 2. Protocolo SSE (Server-Sent Events) O stream é uma sequência de eventos `data:` enviados ao longo da conexão HTTP. ### 2.1 Tipos de Evento | `type` | Quando | Campos | |---|---|---| | `chunk` | Token de texto gerado pela LLM | `content: String` | | `done` | Stream concluído com sucesso | `sessionId: UUID` | | `error` | Erro durante o processamento | `message: String` | ### 2.2 Exemplo de Stream ``` data: {"type":"chunk","content":"O binário"} data: {"type":"chunk","content":" utiliza "} data: {"type":"chunk","content":"as seguintes"} data: {"type":"chunk","content":" funções"} data: {"type":"chunk","content":" para criptografia:"} data: {"type":"chunk","content":"\n\n1. **sub_405000**"} ... data: {"type":"done","sessionId":"550e8400-e29b-..."} ``` ### 2.3 Evento de Erro ``` data: {"type":"error","message":"Ollama is not available. Install ollama and pull gemma4:12b."} ``` Após um evento `error`, a conexão é fechada. Nenhum evento `done` é enviado. ### 2.4 Notas sobre SSE - A conexão é mantida aberta até o stream terminar (`done` ou `error`) - Tokens podem chegar com latência variável (a LLM gera em tempo real) - Cada evento `data:` contém uma linha JSON válida - O frontend deve acumular os `chunk.content` para montar a resposta completa - O backend **já salva** a resposta completa no banco ao final — o frontend não precisa reenviar --- ## 3. Fluxo de Implementação ### 3.1 Diagrama de Sequência ``` Frontend Backend Ollama │ │ │ │ POST /chat/sessions (criar) │ │ ├────────────────────────────────►│ │ │◄────────────────────────────────┤ sessionId │ │ │ │ │ POST /chat/sessions/{id}/messages (SSE) │ ├────────────────────────────────►│ │ │ │ embed query │ │ ├──────────────────────────────►│ │ │◄──────────────────────────────┤ │ │ │ │ │ pgvector search (RAG) │ │ │ build system prompt │ │ │ │ │ │ stream LLM │ │ ├──────────────────────────────►│ │◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤ │◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤ │◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤ │◄──── data: {"type":"done"...}───┤ │ │ │ │ │ │ save assistant message │ │ │ │ │ GET /chat/sessions/{id}/messages (refresh) │ ├────────────────────────────────►│ │ │◄────────────────────────────────┤ full history │ ``` ### 3.2 Estrutura de Componentes (React) ``` BinaryDetailPage ├── ChatPanel ← componente principal do chat │ ├── ChatSessionList ← barra lateral: lista de sessões │ │ ├── ChatSessionItem ← cada sessão (título, modelo, msg count) │ │ └── NewSessionButton ← botão "Nova conversa" │ │ │ └── ChatWindow ← área principal de conversa │ ├── ChatMessageList ← scroll area com mensagens │ │ ├── UserMessage ← bolha do usuário │ │ └── AssistantMessage ← bolha do assistente (com markdown) │ │ │ ├── StreamingMessage ← mensagem sendo gerada (cursor piscando) │ └── ChatInput ← textarea + botão enviar ``` ### 3.3 Exemplo de Hook: `useChatSession` ```typescript interface ChatSession { id: string; binaryId: string; projectId: string; title: string; chatModel: string; messageCount: number; createdAt: string; updatedAt: string; } interface ChatMessage { id: string; sessionId: string; role: 'USER' | 'ASSISTANT'; content: string; tokenCount: number; createdAt: string; } function useChatSession(binaryId: string, projectId: string) { const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); const [messages, setMessages] = useState([]); const [streamingContent, setStreamingContent] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const abortRef = useRef(null); // Listar sessões const loadSessions = async () => { const res = await fetch(`/binaries/${binaryId}/chat/sessions`); const data = await res.json(); setSessions(data); }; // Criar nova sessão const createSession = async (model: string = 'gemma4:12b') => { const res = await fetch( `/binaries/${binaryId}/chat/sessions?projectId=${projectId}&model=${model}`, { method: 'POST' } ); const session = await res.json(); setSessions(prev => [session, ...prev]); setActiveSessionId(session.id); setMessages([]); return session; }; // Carregar histórico const loadMessages = async (sessionId: string) => { const res = await fetch(`/chat/sessions/${sessionId}/messages`); const data = await res.json(); setMessages(data); }; // Enviar mensagem (SSE streaming) const sendMessage = async (content: string) => { if (!activeSessionId) return; // Adiciona mensagem do usuário ao estado local (otimista) const userMsg: ChatMessage = { id: crypto.randomUUID(), sessionId: activeSessionId, role: 'USER', content, tokenCount: 0, createdAt: new Date().toISOString(), }; setMessages(prev => [...prev, userMsg]); setStreamingContent(''); setIsStreaming(true); const controller = new AbortController(); abortRef.current = controller; try { const response = await fetch( `/chat/sessions/${activeSessionId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', }, body: JSON.stringify({ content }), signal: controller.signal, } ); const reader = response.body!.getReader(); const decoder = new TextDecoder(); let fullContent = ''; let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const event = JSON.parse(line.slice(6)); if (event.type === 'chunk') { fullContent += event.content; setStreamingContent(fullContent); } else if (event.type === 'done') { setIsStreaming(false); // Recarrega histórico real para pegar IDs do backend await loadMessages(activeSessionId); } else if (event.type === 'error') { setIsStreaming(false); throw new Error(event.message); } } } } catch (err: any) { if (err.name !== 'AbortError') { setIsStreaming(false); console.error('Chat error:', err); } } }; // Cancelar stream const cancelStream = () => { abortRef.current?.abort(); setIsStreaming(false); }; // Deletar sessão const deleteSession = async (sessionId: string) => { await fetch(`/chat/sessions/${sessionId}`, { method: 'DELETE' }); setSessions(prev => prev.filter(s => s.id !== sessionId)); if (activeSessionId === sessionId) { setActiveSessionId(null); setMessages([]); } }; // Selecionar sessão const selectSession = async (sessionId: string) => { setActiveSessionId(sessionId); await loadMessages(sessionId); }; return { sessions, activeSessionId, messages, streamingContent, isStreaming, loadSessions, createSession, selectSession, sendMessage, cancelStream, deleteSession, }; } ``` ### 3.4 Componente de Chat (React) ```tsx function ChatWindow({ messages, streamingContent, isStreaming, onSend, onCancel, }: { messages: ChatMessage[]; streamingContent: string; isStreaming: boolean; onSend: (content: string) => void; onCancel: () => void; }) { const [input, setInput] = useState(''); const scrollRef = useRef(null); useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, streamingContent]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || isStreaming) return; onSend(input.trim()); setInput(''); }; return (
{messages.map(msg => ( ))} {isStreaming && ( )}