Fix chat session management and add floating widget
- Fix session sync: events now send complete ChatSession data instead of partial ChatSessionSummary, ensuring proper ticket/agent info display - Add session-ended event detection to remove closed sessions from client - Add ChatFloatingWidget component for in-app chat experience - Restrict endSession to ADMIN/MANAGER/AGENT roles only - Improve polling logic to detect new and ended sessions properly 🤖 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
e4f8f465de
commit
88a3b37f2f
5 changed files with 680 additions and 92 deletions
|
|
@ -10,6 +10,8 @@ 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"
|
||||
|
||||
type MachineOs = {
|
||||
name: string
|
||||
|
|
@ -338,6 +340,11 @@ 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
|
||||
const fresh = await invoke<MachineProfile>("collect_machine_profile")
|
||||
|
|
@ -1032,6 +1039,81 @@ const resolvedAppUrl = useMemo(() => {
|
|||
}
|
||||
}, [store, config?.machineId, rustdeskInfo, isRustdeskProvisioning, ensureRustdesk, syncRemoteAccessDirect])
|
||||
|
||||
// Listeners de eventos do chat
|
||||
useEffect(() => {
|
||||
if (!token) return
|
||||
|
||||
let disposed = false
|
||||
const unlisteners: Array<() => void> = []
|
||||
|
||||
// Listener para nova sessao de chat
|
||||
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)
|
||||
}).catch(err => console.error("Falha ao registrar listener session-started:", err))
|
||||
|
||||
// Listener para sessao encerrada
|
||||
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)
|
||||
listen<UnreadUpdateEvent>("raven://chat/unread-update", (event) => {
|
||||
if (disposed) return
|
||||
logDesktop("chat:unread-update", { totalUnread: event.payload.totalUnread, sessionsCount: event.payload.sessions.length })
|
||||
setChatUnreadCount(event.payload.totalUnread)
|
||||
// Atualiza sessoes com dados completos do backend
|
||||
if (event.payload.sessions && event.payload.sessions.length > 0) {
|
||||
setChatSessions(event.payload.sessions)
|
||||
} else if (event.payload.totalUnread === 0) {
|
||||
// Sem sessoes ativas
|
||||
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)
|
||||
listen<NewMessageEvent>("raven://chat/new-message", (event) => {
|
||||
if (disposed) return
|
||||
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) {
|
||||
setChatSessions(event.payload.sessions)
|
||||
}
|
||||
// Abre o widget quando chega nova mensagem
|
||||
if (event.payload.newCount > 0) {
|
||||
setIsChatOpen(true)
|
||||
}
|
||||
}).then(unlisten => {
|
||||
if (disposed) unlisten()
|
||||
else unlisteners.push(unlisten)
|
||||
}).catch(err => console.error("Falha ao registrar listener new-message:", err))
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
unlisteners.forEach(unlisten => unlisten())
|
||||
}
|
||||
}, [token])
|
||||
|
||||
async function register() {
|
||||
if (!profile) return
|
||||
const trimmedCode = provisioningCode.trim().toLowerCase()
|
||||
|
|
@ -1617,6 +1699,17 @@ 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue