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 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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