feat(desktop): add file attachments and native chat window

- Add file upload support in chat (PDF, images, txt, docs, xlsx)
  - Limited to 10MB max file size
  - Only allowed extensions for security
- Use native Windows decorations for chat window
- Remove ChatFloatingWidget (replaced by native window)
- Simplify chat event listeners (window managed by Rust)
- Fix typo "sessao" -> "sessão"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-07 13:09:55 -03:00
parent 2f89fa33fe
commit c217a40030
8 changed files with 537 additions and 104 deletions

View file

@ -1,6 +1,7 @@
import { v } from "convex/values"
import { mutation, query, type MutationCtx, type QueryCtx } from "./_generated/server"
import { action, mutation, query, type MutationCtx, type QueryCtx } from "./_generated/server"
import { ConvexError } from "convex/values"
import { api } from "./_generated/api"
import type { Doc, Id } from "./_generated/dataModel"
import { sha256 } from "@noble/hashes/sha256"
import { bytesToHex as toHex } from "@noble/hashes/utils"
@ -690,3 +691,94 @@ export const getTicketChatHistory = query({
}
},
})
// ============================================
// UPLOAD DE ARQUIVOS (Maquina/Cliente)
// ============================================
// Tipos de arquivo permitidos para upload
const ALLOWED_MIME_TYPES = [
// Imagens
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
// Documentos
"application/pdf",
"text/plain",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
]
const ALLOWED_EXTENSIONS = [
".jpg", ".jpeg", ".png", ".gif", ".webp",
".pdf", ".txt", ".doc", ".docx", ".xls", ".xlsx",
]
// Tamanho maximo: 10MB
const MAX_FILE_SIZE = 10 * 1024 * 1024
// Mutation interna para validar token (usada pela action)
export const validateMachineTokenForUpload = query({
args: { machineToken: v.string() },
handler: async (ctx, args) => {
const tokenHash = hashToken(args.machineToken)
const tokenRecord = await ctx.db
.query("machineTokens")
.withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
.first()
if (!tokenRecord) {
return { valid: false }
}
const machine = await ctx.db.get(tokenRecord.machineId)
if (!machine || machine.status === "REVOKED") {
return { valid: false }
}
return { valid: true, tenantId: tokenRecord.tenantId }
},
})
// Action para gerar URL de upload (validada por token de maquina)
export const generateMachineUploadUrl = action({
args: {
machineToken: v.string(),
fileName: v.string(),
fileType: v.string(),
fileSize: v.number(),
},
handler: async (ctx, args) => {
// Validar token
const validation = await ctx.runQuery(api.liveChat.validateMachineTokenForUpload, {
machineToken: args.machineToken,
})
if (!validation.valid) {
throw new ConvexError("Token de máquina inválido")
}
// Validar tipo de arquivo
const ext = args.fileName.toLowerCase().slice(args.fileName.lastIndexOf("."))
if (!ALLOWED_EXTENSIONS.includes(ext)) {
throw new ConvexError(`Tipo de arquivo não permitido. Permitidos: ${ALLOWED_EXTENSIONS.join(", ")}`)
}
if (!ALLOWED_MIME_TYPES.includes(args.fileType)) {
throw new ConvexError("Tipo MIME não permitido")
}
// Validar tamanho
if (args.fileSize > MAX_FILE_SIZE) {
throw new ConvexError(`Arquivo muito grande. Máximo: ${MAX_FILE_SIZE / 1024 / 1024}MB`)
}
// Gerar URL de upload
const uploadUrl = await ctx.storage.generateUploadUrl()
return { uploadUrl }
},
})