diff --git a/crmDashboard/src/features/sessions/ChatSessionModal.jsx b/crmDashboard/src/features/sessions/ChatSessionModal.jsx new file mode 100644 index 0000000..2c6530c --- /dev/null +++ b/crmDashboard/src/features/sessions/ChatSessionModal.jsx @@ -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 ( + + Chat: {session?.name || session?.phone || session?.id} + + {isLoading && ( + + + + )} + {error && ( + + {error.message} + + )} + + {messages?.map((msg) => { + const parsed = parseMessage(msg.message) + const isAi = parsed?.type === 'ai' + return ( + ({ + 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', + })} + > + {parsed?.content || ''} + + ) + })} + + + setText(e.target.value)} + onKeyDown={handleKeyDown} + disabled={sendMutation.isPending} + multiline + maxRows={4} + /> + + + {sendMutation.isError && ( + + {sendMutation.error?.message || 'Error al enviar'} + + )} + + + + + + ) +} diff --git a/crmDashboard/src/features/sessions/SessionCard.jsx b/crmDashboard/src/features/sessions/SessionCard.jsx index 977f3aa..d1b909f 100644 --- a/crmDashboard/src/features/sessions/SessionCard.jsx +++ b/crmDashboard/src/features/sessions/SessionCard.jsx @@ -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 }) { setAnchor(null)}> + + Chat + {session.block ? ( Desbloquear diff --git a/crmDashboard/src/features/sessions/SessionsPage.jsx b/crmDashboard/src/features/sessions/SessionsPage.jsx index 4d2875c..c640c8e 100644 --- a/crmDashboard/src/features/sessions/SessionsPage.jsx +++ b/crmDashboard/src/features/sessions/SessionsPage.jsx @@ -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)} /> ))} @@ -108,6 +111,10 @@ export default function SessionsPage() { session={blockingSession} onClose={() => setBlockingSession(null)} /> + setChatSession(null)} + /> ) } diff --git a/crmDashboard/src/features/sessions/api.js b/crmDashboard/src/features/sessions/api.js index c991d28..0743526 100644 --- a/crmDashboard/src/features/sessions/api.js +++ b/crmDashboard/src/features/sessions/api.js @@ -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] }) + }, + }) +}