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:
parent
2f89fa33fe
commit
c217a40030
8 changed files with 537 additions and 104 deletions
|
|
@ -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 }
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue