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

@ -10,8 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"
import { cn } from "./lib/utils"
import { ChatApp } from "./chat"
import { DeactivationScreen } from "./components/DeactivationScreen"
import { ChatFloatingWidget } from "./components/ChatFloatingWidget"
import type { ChatSession, SessionStartedEvent, UnreadUpdateEvent, NewMessageEvent, SessionEndedEvent } from "./chat/types"
import type { SessionStartedEvent, UnreadUpdateEvent, NewMessageEvent, SessionEndedEvent } from "./chat/types"
type MachineOs = {
name: string
@ -340,10 +339,6 @@ function App() {
const emailRegex = useRef(/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i)
const isEmailValid = useMemo(() => emailRegex.current.test(collabEmail.trim()), [collabEmail])
// Estados do chat
const [chatSessions, setChatSessions] = useState<ChatSession[]>([])
const [chatUnreadCount, setChatUnreadCount] = useState(0)
const [isChatOpen, setIsChatOpen] = useState(false)
const ensureProfile = useCallback(async () => {
if (profile) return profile
@ -1039,7 +1034,7 @@ const resolvedAppUrl = useMemo(() => {
}
}, [store, config?.machineId, rustdeskInfo, isRustdeskProvisioning, ensureRustdesk, syncRemoteAccessDirect])
// Listeners de eventos do chat
// Listeners de eventos do chat (apenas para logging - a janela nativa e gerenciada pelo Rust)
useEffect(() => {
if (!token) return
@ -1050,14 +1045,6 @@ const resolvedAppUrl = useMemo(() => {
listen<SessionStartedEvent>("raven://chat/session-started", (event) => {
if (disposed) return
logDesktop("chat:session-started", { ticketId: event.payload.session.ticketId, sessionId: event.payload.session.sessionId })
setChatSessions(prev => {
// Evitar duplicatas
if (prev.some(s => s.sessionId === event.payload.session.sessionId)) {
return prev
}
return [...prev, event.payload.session]
})
setIsChatOpen(true) // Abre automaticamente quando agente inicia chat
}).then(unlisten => {
if (disposed) unlisten()
else unlisteners.push(unlisten)
@ -1067,48 +1054,24 @@ const resolvedAppUrl = useMemo(() => {
listen<SessionEndedEvent>("raven://chat/session-ended", (event) => {
if (disposed) return
logDesktop("chat:session-ended", { ticketId: event.payload.ticketId, sessionId: event.payload.sessionId })
setChatSessions(prev => prev.filter(s => s.sessionId !== event.payload.sessionId))
}).then(unlisten => {
if (disposed) unlisten()
else unlisteners.push(unlisten)
}).catch(err => console.error("Falha ao registrar listener session-ended:", err))
// Listener para atualizacao de mensagens nao lidas (sincroniza sessoes completas)
// Listener para atualizacao de mensagens nao lidas
listen<UnreadUpdateEvent>("raven://chat/unread-update", (event) => {
if (disposed) return
console.log("[CHAT DEBUG] unread-update recebido:", JSON.stringify(event.payload, null, 2))
logDesktop("chat:unread-update", { totalUnread: event.payload.totalUnread, sessionsCount: event.payload.sessions?.length ?? 0 })
setChatUnreadCount(event.payload.totalUnread)
// Atualiza sessoes com dados completos do backend
if (event.payload.sessions && event.payload.sessions.length > 0) {
console.log("[CHAT DEBUG] Atualizando chatSessions com", event.payload.sessions.length, "sessoes")
setChatSessions(event.payload.sessions)
} else if (event.payload.totalUnread === 0) {
// Sem sessoes ativas
console.log("[CHAT DEBUG] Sem sessoes ativas, limpando chatSessions")
setChatSessions([])
}
}).then(unlisten => {
if (disposed) unlisten()
else unlisteners.push(unlisten)
}).catch(err => console.error("Falha ao registrar listener unread-update:", err))
// Listener para nova mensagem (abre widget se fechado)
// Listener para nova mensagem (a janela de chat nativa e aberta automaticamente pelo Rust)
listen<NewMessageEvent>("raven://chat/new-message", (event) => {
if (disposed) return
console.log("[CHAT DEBUG] new-message recebido:", JSON.stringify(event.payload, null, 2))
logDesktop("chat:new-message", { totalUnread: event.payload.totalUnread, newCount: event.payload.newCount })
setChatUnreadCount(event.payload.totalUnread)
// Atualiza sessoes com dados completos do backend
if (event.payload.sessions && event.payload.sessions.length > 0) {
console.log("[CHAT DEBUG] Atualizando chatSessions com", event.payload.sessions.length, "sessoes")
setChatSessions(event.payload.sessions)
}
// Abre o widget quando chega nova mensagem
if (event.payload.newCount > 0) {
console.log("[CHAT DEBUG] Nova mensagem! Abrindo widget...")
setIsChatOpen(true)
}
}).then(unlisten => {
if (disposed) unlisten()
else unlisteners.push(unlisten)
@ -1706,16 +1669,6 @@ const resolvedAppUrl = useMemo(() => {
</div>
)}
{/* Chat Widget Flutuante - aparece quando provisionado e ha sessoes ativas */}
{token && isMachineActive && chatSessions.length > 0 && (
<ChatFloatingWidget
sessions={chatSessions}
totalUnread={chatUnreadCount}
isOpen={isChatOpen}
onToggle={() => setIsChatOpen(!isChatOpen)}
onMinimize={() => setIsChatOpen(false)}
/>
)}
</div>
)
}