diff --git a/public/app.js b/public/app.js index 8984625..c7a3f86 100644 --- a/public/app.js +++ b/public/app.js @@ -301,6 +301,118 @@ document.addEventListener('DOMContentLoaded', () => { window.speechSynthesis.speak(utterance); }; + // ========================================================================== + // ANEXOS — Upload de imagens e documentos + // ========================================================================== + + const fileInput = document.getElementById('fileInput'); + const btnAttach = document.getElementById('btnAttach'); + let attachedFiles = []; + + // Criar preview container se não existir + let attachPreview = document.querySelector('.attachments-preview'); + + const ensureAttachPreview = () => { + if (!attachPreview) { + attachPreview = document.createElement('div'); + attachPreview.className = 'attachments-preview'; + document.getElementById('chatForm').appendChild(attachPreview); + } + }; + + // Botão de anexar → abre seletor de arquivos + btnAttach.addEventListener('click', () => { + fileInput.click(); + }); + + // Quando arquivos são selecionados + fileInput.addEventListener('change', () => { + const files = Array.from(fileInput.files); + if (files.length === 0) return; + + files.forEach(file => { + // Limite de 10MB por arquivo + if (file.size > 10 * 1024 * 1024) { + alert(`Arquivo "${file.name}" é muito grande. Máximo: 10MB.`); + return; + } + attachedFiles.push(file); + }); + + renderAttachPreview(); + fileInput.value = ''; // reset para permitir selects again + }); + + // Arrastar e soltar na área do chat + const chatContainer = document.querySelector('.chat-container'); + chatContainer.addEventListener('dragover', (e) => { + e.preventDefault(); + chatContainer.classList.add('drag-over'); + }); + chatContainer.addEventListener('dragleave', () => { + chatContainer.classList.remove('drag-over'); + }); + chatContainer.addEventListener('drop', (e) => { + e.preventDefault(); + chatContainer.classList.remove('drag-over'); + const files = Array.from(e.dataTransfer.files); + files.forEach(file => { + if (file.size <= 10 * 1024 * 1024) { + attachedFiles.push(file); + } + }); + renderAttachPreview(); + }); + + // Renderizar preview dos anexos + const renderAttachPreview = () => { + ensureAttachPreview(); + attachPreview.innerHTML = ''; + + if (attachedFiles.length === 0) { + attachPreview.style.display = 'none'; + btnAttach.classList.remove('has-files'); + return; + } + + btnAttach.classList.add('has-files'); + attachPreview.style.display = 'flex'; + + attachedFiles.forEach((file, index) => { + const item = document.createElement('div'); + item.className = 'attachment-item'; + + const isImage = file.type.startsWith('image/'); + let previewHtml = ''; + + if (isImage) { + const url = URL.createObjectURL(file); + previewHtml = `${file.name}`; + } else { + previewHtml = ` + + `; + } + + item.innerHTML = ` + ${previewHtml} + ${file.name} + + `; + + attachPreview.appendChild(item); + }); + + // Botões de remover + attachPreview.querySelectorAll('.remove-attachment').forEach(btn => { + btn.addEventListener('click', () => { + const idx = parseInt(btn.dataset.index); + attachedFiles.splice(idx, 1); + renderAttachPreview(); + }); + }); + }; + // ========================================================================== // ATALHOS DE TECLADO // ========================================================================== @@ -773,8 +885,41 @@ document.addEventListener('DOMContentLoaded', () => { }; // Função principal de envio de mensagens - const handleSendMessage = async (text, skipUserAppend = false) => { - if (!text || isGenerating) return; + const handleSendMessage = async (text, skipUserAppend = false, files = null) => { + if (!text && (!files || files.length === 0)) return; + + // Se não recebe files por parâmetro, usa os anexados globalmente + const filesToSend = files || attachedFiles; + let fullText = text || ''; + + // Processar anexos: imagens viram descrição em texto (o modelo não tem visão) + if (filesToSend.length > 0) { + for (const file of filesToSend) { + if (file.type.startsWith('image/')) { + const reader = new FileReader(); + const base64 = await new Promise((resolve) => { + reader.onload = (e) => resolve(e.target.result); + reader.readAsDataURL(file); + }); + fullText += `\n\n[Imagem anexada: ${file.name}]\n\`\`\`\n${base64}\n\`\`\``; + } else { + const reader = new FileReader(); + const content = await new Promise((resolve) => { + reader.onload = (e) => resolve(e.target.result); + reader.readAsText(file); + }); + const truncated = content.length > 2000 ? content.substring(0, 2000) + '...[arquivo truncado]' : content; + fullText += `\n\n[Documento anexo: ${file.name}]\n\`\`\`\n${truncated}\n\`\`\``; + } + } + // Limpar anexos após envio + attachedFiles = []; + if (attachPreview) { + attachPreview.innerHTML = ''; + attachPreview.style.display = 'none'; + btnAttach.classList.remove('has-files'); + } + } isGenerating = true; btnSend.disabled = true; @@ -790,7 +935,7 @@ document.addEventListener('DOMContentLoaded', () => { // Criar o chat se for novo if (!currentChatId) { const newId = 'chat_' + Date.now(); - const firstLine = text.split('\n')[0]; + const firstLine = fullText.split('\n')[0]; const title = firstLine.length > 26 ? firstLine.substring(0, 26) + '...' : firstLine; chats.push({ @@ -811,10 +956,10 @@ document.addEventListener('DOMContentLoaded', () => { // Adicionar mensagem do usuário no estado e na tela (pula se for regeneração) if (!skipUserAppend) { - const userMsg = { role: 'user', content: text }; + const userMsg = { role: 'user', content: fullText }; chat.messages.push(userMsg); chat.updatedAt = Date.now(); - appendMessageUI('user', text); + appendMessageUI('user', fullText); scrollToBottom(); saveChats(); renderHistory(); diff --git a/public/index.html b/public/index.html index b09b8f6..482cac7 100644 --- a/public/index.html +++ b/public/index.html @@ -154,6 +154,12 @@
+ +