feat(front): kinda added chat feature

This commit is contained in:
2026-05-31 23:21:23 -06:00
parent 767317e538
commit 658b9f0257
4 changed files with 212 additions and 1 deletions
@@ -0,0 +1,139 @@
import { useState, useRef, useEffect } from 'react'
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Box,
Typography,
CircularProgress,
Alert,
} from '@mui/material'
import { useChatHistory, useSendMessage } from './api'
export default function ChatSessionModal({ session, onClose }) {
const [text, setText] = useState('')
const scrollRef = useRef(null)
const { data: messages, isLoading, error } = useChatHistory(session?.phone)
const sendMutation = useSendMessage()
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [messages])
const handleSend = async () => {
if (!text.trim() || !session) return
await sendMutation.mutateAsync({ sessionId: session.phone, message: text.trim() })
setText('')
}
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
const parseMessage = (msg) => {
if (!msg) return null
if (typeof msg === 'string') {
try {
return JSON.parse(msg)
} catch {
return { type: 'ai', content: msg }
}
}
return msg
}
return (
<Dialog open={!!session} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Chat: {session?.name || session?.phone || session?.id}</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', height: '60vh', p: 2 }}>
{isLoading && (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', flex: 1 }}>
<CircularProgress />
</Box>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error.message}
</Alert>
)}
<Box
ref={scrollRef}
sx={{
flex: 1,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 1,
mb: 2,
}}
>
{messages?.map((msg) => {
const parsed = parseMessage(msg.message)
const isAi = parsed?.type === 'ai'
return (
<Box
key={msg.id}
sx={(theme) => ({
alignSelf: isAi ? 'flex-start' : 'flex-end',
bgcolor: isAi
? theme.palette.mode === 'dark'
? 'grey.800'
: 'grey.200'
: 'primary.main',
color: isAi
? theme.palette.mode === 'dark'
? 'grey.100'
: 'text.primary'
: 'primary.contrastText',
p: 1.5,
borderRadius: 2,
maxWidth: '80%',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
})}
>
<Typography variant="body2">{parsed?.content || ''}</Typography>
</Box>
)
})}
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
fullWidth
size="small"
placeholder="Escribe un mensaje..."
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
disabled={sendMutation.isPending}
multiline
maxRows={4}
/>
<Button
variant="contained"
onClick={handleSend}
disabled={!text.trim() || sendMutation.isPending}
>
{sendMutation.isPending ? <CircularProgress size={20} color="inherit" /> : 'Enviar'}
</Button>
</Box>
{sendMutation.isError && (
<Alert severity="error" sx={{ mt: 1 }}>
{sendMutation.error?.message || 'Error al enviar'}
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cerrar</Button>
</DialogActions>
</Dialog>
)
}
@@ -20,7 +20,7 @@ function formatDate(d) {
}
}
export default function SessionCard({ session, onBlock }) {
export default function SessionCard({ session, onBlock, onChat }) {
const [anchor, setAnchor] = useState(null)
const deleteMutation = useDeleteSession()
const unblockMutation = useUnblockSession()
@@ -37,6 +37,11 @@ export default function SessionCard({ session, onBlock }) {
unblockMutation.mutate({ phone: session.phone })
}
const handleChat = () => {
setAnchor(null)
onChat(session)
}
const statusColor = session.finished ? 'success' : 'warning'
const statusText = session.finished ? 'Finalizada' : 'Activa'
@@ -83,6 +88,9 @@ export default function SessionCard({ session, onBlock }) {
</Box>
</CardContent>
<Menu anchorEl={anchor} open={Boolean(anchor)} onClose={() => setAnchor(null)}>
<MenuItem onClick={handleChat}>
Chat
</MenuItem>
{session.block ? (
<MenuItem onClick={handleUnblock} sx={{ color: 'error.main' }}>
Desbloquear
@@ -16,11 +16,13 @@ import { useCampaigns } from '../campaigns/api'
import { useSessions } from './api'
import SessionCard from './SessionCard'
import BlockSessionModal from './BlockSessionModal'
import ChatSessionModal from './ChatSessionModal'
export default function SessionsPage() {
const [campaignFilter, setCampaignFilter] = useState('')
const [search, setSearch] = useState('')
const [blockingSession, setBlockingSession] = useState(null)
const [chatSession, setChatSession] = useState(null)
const { data: campaigns } = useCampaigns()
const { data: sessions, isLoading, refetch } = useSessions(campaignFilter)
@@ -97,6 +99,7 @@ export default function SessionsPage() {
key={s.id}
session={s}
onBlock={(session) => setBlockingSession(session)}
onChat={(session) => setChatSession(session)}
/>
))}
</Box>
@@ -108,6 +111,10 @@ export default function SessionsPage() {
session={blockingSession}
onClose={() => setBlockingSession(null)}
/>
<ChatSessionModal
session={chatSession}
onClose={() => setChatSession(null)}
/>
</Box>
)
}
+57
View File
@@ -73,3 +73,60 @@ export function useDeleteSession() {
},
})
}
export function useChatHistory(sessionId) {
return useQuery({
queryKey: ['chatHistory', sessionId],
queryFn: async () => {
if (!sessionId) return []
const { data, error } = await supabase
.from('n8n_chat_histories')
.select('*')
.eq('session_id', sessionId)
.order('id', { ascending: true })
if (error) throw new Error(error.message)
return data || []
},
enabled: !!sessionId,
})
}
const WEBHOOK_URL = 'https://n8n.beroth.moe/webhook/send-msg'
const XTOKEN = 'PLACEHOLDER_XTOKEN'
export function useSendMessage() {
const qc = useQueryClient()
return useMutation({
mutationFn: async ({ sessionId, message }) => {
const res = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'xtoken': XTOKEN,
},
body: JSON.stringify({ phone: sessionId, msg: message }),
})
if (!res.ok) {
const errText = await res.text().catch(() => 'Unknown error')
throw new Error(`Webhook error ${res.status}: ${errText}`)
}
const { error } = await supabase
.from('n8n_chat_histories')
.insert({
session_id: sessionId,
message: {
type: 'ai',
content: message,
additional_kwargs: {},
response_metadata: {},
tool_calls: [],
invalid_tool_calls: [],
},
})
if (error) throw new Error(error.message)
},
onSuccess: (_, { sessionId }) => {
qc.invalidateQueries({ queryKey: ['chatHistory', sessionId] })
},
})
}