frontend/frontend-guide.md

20 KiB
Raw Blame History

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):

{
  "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):

[
  {
    "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):

[
  {
    "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:

{
  "content": "Quais funções nesse binário lidam com criptografia?"
}
Campo Tipo Validação
content String 14000 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

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<ChatSession[]>([]);
  const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [streamingContent, setStreamingContent] = useState('');
  const [isStreaming, setIsStreaming] = useState(false);
  const abortRef = useRef<AbortController | null>(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)

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<HTMLDivElement>(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 (
    <div className="chat-window">
      <div className="messages">
        {messages.map(msg => (
          <MessageBubble key={msg.id} role={msg.role} content={msg.content} />
        ))}
        {isStreaming && (
          <MessageBubble
            role="ASSISTANT"
            content={streamingContent}
            isStreaming
          />
        )}
        <div ref={scrollRef} />
      </div>

      <form onSubmit={handleSubmit} className="chat-input">
        <textarea
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="Pergunte sobre funções, protocolos, estruturas..."
          disabled={isStreaming}
          rows={2}
        />
        {isStreaming ? (
          <button type="button" onClick={onCancel}>Parar</button>
        ) : (
          <button type="submit" disabled={!input.trim()}>Enviar</button>
        )}
      </form>
    </div>
  );
}

4. Tratamento de Erros

Cenário Código Ação Recomendada
Sessão não encontrada 404 Redirecionar para lista de binários
Binário não analisado Embeddings não gerados → exibir mensagem "Execute a análise estática primeiro"
Ollama offline error SSE Exibir "Ollama não está rodando. Inicie com ollama serve"
Modelo não baixado error SSE Exibir "Modelo não encontrado. Execute ollama pull gemma4:12b"
Timeout do stream Conexão fechada Mostrar mensagem parcial + botão "Tentar novamente"
Conteúdo vazio 400 Validar no frontend: mínimo 1 caractere
Conteúdo > 4000 chars 400 Limitar textarea a 4000 caracteres

Exemplo de Error Boundary

function getErrorMessage(error: string): string {
  if (error.includes('Ollama is not available')) {
    return 'Servidor LLM local (Ollama) não está rodando. Execute `ollama serve` no terminal.';
  }
  if (error.includes('Install ollama and pull')) {
    return `Modelo LLM não encontrado. Execute no terminal:\nollama pull gemma4:12b`;
  }
  return `Erro ao processar a resposta: ${error}`;
}

5. Considerações de UX

5.1 Estados da UI

Estado O que mostrar
Binário sem análise "Execute a análise estática antes de usar o chat."
Sem embeddings "Aguarde a geração dos embeddings..." (polling do job GENERATE_EMBEDDINGS)
Nenhuma sessão Botão "Iniciar conversa" proeminente
Streaming Bolha do assistente com cursor piscando (), botão "Parar"
Stream concluído Mensagem completa renderizada com markdown
Erro Banner de erro com ação sugerida

5.2 Renderização de Markdown

A resposta da LLM frequentemente contém markdown (negrito, listas, blocos de código). Use uma biblioteca como react-markdown com syntax highlighting para assembly:

import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';

function MessageBubble({ content, role, isStreaming }: Props) {
  return (
    <div className={`bubble ${role.toLowerCase()}`}>
      <ReactMarkdown
        components={{
          code({ className, children, ...props }) {
            const match = /language-(\w+)/.exec(className || '');
            return match ? (
              <SyntaxHighlighter language={match[1]} PreTag="div">
                {String(children).replace(/\n$/, '')}
              </SyntaxHighlighter>
            ) : (
              <code className={className} {...props}>{children}</code>
            );
          },
        }}
      >
        {content}
      </ReactMarkdown>
      {isStreaming && <span className="cursor"></span>}
    </div>
  );
}

5.3 Gerenciamento de Scroll

  • Scroll automático para o final ao receber novos tokens
  • Se o usuário scrollar para cima manualmente, não força scroll (respeita leitura)
  • Retoma auto-scroll quando usuário scrolla de volta ao final
function useAutoScroll(deps: any[]) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [userScrolledUp, setUserScrolledUp] = useState(false);

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    const handleScroll = () => {
      const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
      setUserScrolledUp(!isAtBottom);
    };

    el.addEventListener('scroll', handleScroll);
    return () => el.removeEventListener('scroll', handleScroll);
  }, []);

  useEffect(() => {
    if (!userScrolledUp) {
      containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' });
    }
  }, deps);

  return containerRef;
}

5.4 Polling de Status dos Embeddings

Antes de habilitar o chat, verifique se os embeddings foram gerados:

async function hasEmbeddings(binaryId: string): Promise<boolean> {
  // Verifica se existem chunks para este binário
  // Opção 1: endpoint dedicado (a ser adicionado)
  // Opção 2: verificar jobs — se existe GENERATE_EMBEDDINGS COMPLETED

  const res = await fetch(`/jobs?binaryId=${binaryId}&type=GENERATE_EMBEDDINGS&status=COMPLETED`);
  const jobs = await res.json();
  return jobs.length > 0;
}

6. Resumo de Integração

Preparação:
  1. Upload binário → POST /projects/{id}/binaries?engine=IDA5
  2. Aguardar análise → polling GET /jobs?binaryId={id}&type=STATIC_ANALYSIS
  3. Aguardar embeddings → polling GET /jobs?binaryId={id}&type=GENERATE_EMBEDDINGS

Chat:
  4. Criar sessão → POST /binaries/{id}/chat/sessions?projectId={pid}
  5. Enviar mensagem → POST /chat/sessions/{sid}/messages (SSE stream)
  6. Renderizar tokens → acumular data.type=chunk
  7. Finalizar → data.type=done → recarregar histórico
  8. Histórico → GET /chat/sessions/{sid}/messages (ao voltar à sessão)

Gerenciamento:
  9. Listar sessões → GET /binaries/{id}/chat/sessions
  10. Deletar sessão → DELETE /chat/sessions/{sid}

7. Endpoints de Suporte

Estes endpoints já existem e são úteis para polling de status:

Jobs

GET /jobs?projectId={projectId}&type=GENERATE_EMBEDDINGS&status=COMPLETED

Retorna jobs do tipo GENERATE_EMBEDDINGS concluídos. Use para verificar se os embeddings estão prontos.

GET /jobs?binaryId={binaryId}&type=GENERATE_EMBEDDINGS

Status possíveis: ENQUEUED, STARTED, IN_PROGRESS, COMPLETED, FAILED.

Funções (para referência no chat)

GET /analysis/{analysisId}/functions?page=0&size=50
GET /functions/{functionId}
GET /functions/{functionId}/callers
GET /functions/{functionId}/callees

O frontend pode usar esses endpoints para permitir que o usuário clique em uma função mencionada na resposta e veja seus detalhes (assembly, xrefs, labels).