From 0464501dd47469b9bc1bb4c16368c0dd77918a05 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Thu, 11 Jun 2026 22:44:49 -0300 Subject: [PATCH] feat(tooltip): add tooltip component and provider --- frontend-guide.md | 671 --------------------------- package-lock.json | 368 +++++++++++++++ package.json | 1 + src/components/ui/tooltip.tsx | 28 ++ src/features/binary/AIChatPanel.tsx | 41 +- src/features/binary/FunctionTree.tsx | 119 ++--- src/providers/Providers.tsx | 7 +- 7 files changed, 504 insertions(+), 731 deletions(-) delete mode 100644 frontend-guide.md create mode 100644 src/components/ui/tooltip.tsx diff --git a/frontend-guide.md b/frontend-guide.md deleted file mode 100644 index f8311cf..0000000 --- a/frontend-guide.md +++ /dev/null @@ -1,671 +0,0 @@ -# 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 && ( - - )} -
-
- -
-