feat(front): kinda added chat feature
This commit is contained in:
@@ -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 [anchor, setAnchor] = useState(null)
|
||||||
const deleteMutation = useDeleteSession()
|
const deleteMutation = useDeleteSession()
|
||||||
const unblockMutation = useUnblockSession()
|
const unblockMutation = useUnblockSession()
|
||||||
@@ -37,6 +37,11 @@ export default function SessionCard({ session, onBlock }) {
|
|||||||
unblockMutation.mutate({ phone: session.phone })
|
unblockMutation.mutate({ phone: session.phone })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChat = () => {
|
||||||
|
setAnchor(null)
|
||||||
|
onChat(session)
|
||||||
|
}
|
||||||
|
|
||||||
const statusColor = session.finished ? 'success' : 'warning'
|
const statusColor = session.finished ? 'success' : 'warning'
|
||||||
const statusText = session.finished ? 'Finalizada' : 'Activa'
|
const statusText = session.finished ? 'Finalizada' : 'Activa'
|
||||||
|
|
||||||
@@ -83,6 +88,9 @@ export default function SessionCard({ session, onBlock }) {
|
|||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<Menu anchorEl={anchor} open={Boolean(anchor)} onClose={() => setAnchor(null)}>
|
<Menu anchorEl={anchor} open={Boolean(anchor)} onClose={() => setAnchor(null)}>
|
||||||
|
<MenuItem onClick={handleChat}>
|
||||||
|
Chat
|
||||||
|
</MenuItem>
|
||||||
{session.block ? (
|
{session.block ? (
|
||||||
<MenuItem onClick={handleUnblock} sx={{ color: 'error.main' }}>
|
<MenuItem onClick={handleUnblock} sx={{ color: 'error.main' }}>
|
||||||
Desbloquear
|
Desbloquear
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import { useCampaigns } from '../campaigns/api'
|
|||||||
import { useSessions } from './api'
|
import { useSessions } from './api'
|
||||||
import SessionCard from './SessionCard'
|
import SessionCard from './SessionCard'
|
||||||
import BlockSessionModal from './BlockSessionModal'
|
import BlockSessionModal from './BlockSessionModal'
|
||||||
|
import ChatSessionModal from './ChatSessionModal'
|
||||||
|
|
||||||
export default function SessionsPage() {
|
export default function SessionsPage() {
|
||||||
const [campaignFilter, setCampaignFilter] = useState('')
|
const [campaignFilter, setCampaignFilter] = useState('')
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [blockingSession, setBlockingSession] = useState(null)
|
const [blockingSession, setBlockingSession] = useState(null)
|
||||||
|
const [chatSession, setChatSession] = useState(null)
|
||||||
const { data: campaigns } = useCampaigns()
|
const { data: campaigns } = useCampaigns()
|
||||||
const { data: sessions, isLoading, refetch } = useSessions(campaignFilter)
|
const { data: sessions, isLoading, refetch } = useSessions(campaignFilter)
|
||||||
|
|
||||||
@@ -97,6 +99,7 @@ export default function SessionsPage() {
|
|||||||
key={s.id}
|
key={s.id}
|
||||||
session={s}
|
session={s}
|
||||||
onBlock={(session) => setBlockingSession(session)}
|
onBlock={(session) => setBlockingSession(session)}
|
||||||
|
onChat={(session) => setChatSession(session)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -108,6 +111,10 @@ export default function SessionsPage() {
|
|||||||
session={blockingSession}
|
session={blockingSession}
|
||||||
onClose={() => setBlockingSession(null)}
|
onClose={() => setBlockingSession(null)}
|
||||||
/>
|
/>
|
||||||
|
<ChatSessionModal
|
||||||
|
session={chatSession}
|
||||||
|
onClose={() => setChatSession(null)}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user