Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import './App.css';
const Login = lazy(() => import('./pages/Login').then(m => ({ default: m.Login })));
const Dashboard = lazy(() => import('./pages/Dashboard').then(m => ({ default: m.Dashboard })));
const Sessions = lazy(() => import('./pages/Sessions').then(m => ({ default: m.Sessions })));
const Chats = lazy(() => import('./pages/Chats').then(m => ({ default: m.Chats })));
const Webhooks = lazy(() => import('./pages/Webhooks').then(m => ({ default: m.Webhooks })));
const Logs = lazy(() => import('./pages/Logs').then(m => ({ default: m.Logs })));
const ApiKeys = lazy(() => import('./pages/ApiKeys').then(m => ({ default: m.ApiKeys })));
const MessageTester = lazy(() => import('./pages/MessageTester').then(m => ({ default: m.MessageTester })));
const Infrastructure = lazy(() => import('./pages/Infrastructure').then(m => ({ default: m.Infrastructure })));
const Plugins = lazy(() => import('./pages/Plugins'));


const queryClient = new QueryClient({
defaultOptions: {
queries: {
Expand Down Expand Up @@ -101,6 +103,7 @@ function AppContent() {
<Route path="/" element={<Layout onLogout={handleLogout} userRole={role} />}>
<Route index element={<Dashboard />} />
<Route path="sessions" element={<Sessions />} />
<Route path="chats" element={<Chats />} />
<Route path="webhooks" element={<Webhooks />} />
{role === 'admin' && <Route path="api-keys" element={<ApiKeys />} />}
<Route path="logs" element={<Logs />} />
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ChevronLeft,
ChevronRight,
Languages,
MessageSquare,
} from 'lucide-react';
import { useTheme } from '../hooks/useTheme';
import { type UserRole } from '../hooks/useRole';
Expand All @@ -33,6 +34,7 @@ interface LayoutProps {
const allNavItems = [
{ to: '/', icon: LayoutDashboard, key: 'dashboard' as const, adminOnly: false },
{ to: '/sessions', icon: Smartphone, key: 'sessions' as const, adminOnly: false },
{ to: '/chats', icon: MessageSquare, key: 'chats' as const, adminOnly: false },
{ to: '/webhooks', icon: Webhook, key: 'webhooks' as const, adminOnly: false },
{ to: '/api-keys', icon: Key, key: 'apiKeys' as const, adminOnly: true },
{ to: '/message-tester', icon: Send, key: 'messageTester' as const, adminOnly: false },
Expand Down
11 changes: 11 additions & 0 deletions dashboard/src/hooks/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const queryKeys = {
sessions: ['sessions'] as const,
sessionStats: ['sessions', 'stats'] as const,
sessionGroups: (sessionId: string) => ['sessions', sessionId, 'groups'] as const,
sessionChats: (sessionId: string) => ['sessions', sessionId, 'chats'] as const,
webhooks: ['webhooks'] as const,
apiKeys: ['apiKeys'] as const,
logs: (params: { severity?: string; page: number; limit: number }) =>
Expand Down Expand Up @@ -52,6 +53,16 @@ export function useSessionGroupsQuery(sessionId: string, enabled: boolean) {
});
}

export function useSessionChatsQuery(sessionId: string, enabled: boolean) {
return useQuery({
queryKey: queryKeys.sessionChats(sessionId),
queryFn: () => sessionApi.getChats(sessionId),
enabled: enabled && !!sessionId,
staleTime: 10_000,
});
}


export function useCreateSessionMutation() {
const queryClient = useQueryClient();
return useMutation({
Expand Down
114 changes: 99 additions & 15 deletions dashboard/src/hooks/useWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,43 @@ interface MessageEvent {
timestamp: string;
}

interface MessageAckEvent {
sessionId: string;
messageId: string;
ack: number;
ackName: string;
chatId?: string;
timestamp: string;
}

interface MessageReactionEvent {
sessionId: string;
messageId: string;
chatId: string;
reaction: string;
senderId: string;
reactions: Record<string, string>;
timestamp: string;
}

interface MessageRevokedEvent {
sessionId: string;
id: string;
chatId: string;
from: string;
to: string;
body: string;
type: string;
timestamp: number;
}

interface WebSocketEvents {
onSessionStatus?: (event: SessionStatusEvent) => void;
onQRCode?: (event: QRCodeEvent) => void;
onMessage?: (event: MessageEvent) => void;
onMessageAck?: (event: MessageAckEvent) => void;
onMessageReaction?: (event: MessageReactionEvent) => void;
onMessageRevoked?: (event: MessageRevokedEvent) => void;
}

// Use current origin for WebSocket (goes through nginx proxy in Docker)
Expand Down Expand Up @@ -75,6 +108,25 @@ export function useWebSocket(events: WebSocketEvents = {}) {
});
}, []);

const subscribe = useCallback((sessionId: string, eventsList: string[]) => {
if (socketRef.current?.connected) {
socketRef.current.emit('message', {
type: 'subscribe',
sessionId,
events: eventsList,
});
}
}, []);

const unsubscribe = useCallback((sessionId: string) => {
if (socketRef.current?.connected) {
socketRef.current.emit('message', {
type: 'unsubscribe',
sessionId,
});
}
}, []);

useEffect(() => {
connect();

Expand All @@ -92,24 +144,56 @@ export function useWebSocket(events: WebSocketEvents = {}) {

const socket = socketRef.current;

if (events.onSessionStatus) {
socket.on('session:status', events.onSessionStatus);
}

if (events.onQRCode) {
socket.on('session:qr', events.onQRCode);
}
const handleIncomingMessage = (msg: any) => {
if (msg && msg.type === 'event' && msg.payload) {
const { event, sessionId, data } = msg.payload;
if (event === 'session.status' && events.onSessionStatus) {
events.onSessionStatus({ sessionId, status: data.status, timestamp: msg.timestamp });
} else if (event === 'session.qr' && events.onQRCode) {
events.onQRCode({ sessionId, qrCode: data.qrCode, timestamp: msg.timestamp });
} else if ((event === 'message.received' || event === 'message.sent') && events.onMessage) {
events.onMessage({ sessionId, message: data, timestamp: msg.timestamp });
} else if (event === 'message.ack' && events.onMessageAck) {
events.onMessageAck({
sessionId,
messageId: data.messageId,
ack: data.ack,
ackName: data.ackName,
chatId: data.chatId,
timestamp: msg.timestamp,
});
} else if (event === 'message.reaction' && events.onMessageReaction) {
events.onMessageReaction({
sessionId,
messageId: data.messageId,
chatId: data.chatId,
reaction: data.reaction,
senderId: data.senderId,
reactions: data.reactions,
timestamp: msg.timestamp,
});
} else if (event === 'message.revoked' && events.onMessageRevoked) {
events.onMessageRevoked({
sessionId,
id: data.id,
chatId: data.chatId,
from: data.from,
to: data.to,
body: data.body,
type: data.type,
timestamp: data.timestamp,
});
}
}
};

if (events.onMessage) {
socket.on('session:message', events.onMessage);
}
socket.on('message', handleIncomingMessage);

return () => {
socket.off('session:status');
socket.off('session:qr');
socket.off('session:message');
socket.off('message', handleIncomingMessage);
};
}, [events.onSessionStatus, events.onQRCode, events.onMessage]);
}, [events]);

return { isConnected };
return { isConnected, subscribe, unsubscribe };
}

1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"nav": {
"dashboard": "Dashboard",
"sessions": "Sessions",
"chats": "Chats",
"webhooks": "Webhooks",
"apiKeys": "API Keys",
"messageTester": "Message Tester",
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/i18n/locales/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"nav": {
"dashboard": "דאשבורד",
"sessions": "Sessions",
"chats": "שיחות",
"webhooks": "Webhooks",
"apiKeys": "מפתחות API",
"messageTester": "בדיקת הודעות",
Expand Down
Loading