672 lines
20 KiB
Markdown
672 lines
20 KiB
Markdown
# 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 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)
|
||
|
||
```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<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
|
||
|
||
```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).
|