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

          switch (event.type) {
            case 'chunk':
              fullContent += event.content;
              setStreamingContent(fullContent);
              break;
            case 'tool_call_start':
              setToolStatus(`Calling ${event.tool}...`);
              break;
            case 'tool_call_end':
              setToolStatus(
                `${event.tool} completed (${event.durationMs}ms): ${event.resultSummary}`
              );
              break;
            case 'thinking':
              setToolStatus(event.message);
              break;
            case 'done':
              setIsStreaming(false);
              setToolStatus(null);
              await loadMessages(activeSessionId);
              break;
            case 'error':
              setIsStreaming(false);
              setToolStatus(null);
              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,
    toolStatus,
    loadSessions,
    createSession,
    selectSession,
    sendMessage,
    cancelStream,
    deleteSession,
  };
}
```

### 3.4 Componente de Chat (React)

```tsx
function ChatWindow({
  messages,
  streamingContent,
  isStreaming,
  toolStatus,
  onSend,
  onCancel,
}: {
  messages: ChatMessage[];
  streamingContent: string;
  isStreaming: boolean;
  toolStatus: string | null;
  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} />
        ))}
        {toolStatus && (
          <div className="tool-status">
            <span className="spinner" /> {toolStatus}
          </div>
        )}
        {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

```typescript
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:

```tsx
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

```typescript
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:

```typescript
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).
