Чати
Управление чатами между клиентами и AI-ассистентами через различные мессенджеры.
Обзор
Функциональность чатов включает:
- Просмотр списка всех чатов с пагинацией
- Фильтрация по компании и мессенджеру
- Отображение последнего сообщения каждого чата (messages/last)
- Детальный просмотр сообщений чата
- Real-time обновления через WebSocket
- Расшифровка зашифрованных сообщений (AES CBC)
- Просмотр системных сообщений и tool calls
Архитектура
Сервисы
ChatsService → GET/DELETE /admin/chats
MessagesService → GET /admin/messages, GET /admin/messages/last
EventsService → Центральная шина событий (Subject-based)
WebsocketsService → Socket.IO подключение, проброс событий в EventsService
CryptoService → Расшифровка текста сообщенийПотоки данных
┌─────────────────────────────────────────────┐
│ WebSocket Flow │
├─────────────────────────────────────────────┤
│ Backend (Socket.IO Server) │
│ ↓ │
│ WebsocketsService.connect() │
│ ↓ │
│ socket.on("event", AppEvent) │
│ ↓ │
│ EventsService.emit(type, payload) │
│ ↓ │
│ Components: EventsService.on(EVENT_TYPE) │
└─────────────────────────────────────────────┘WebSocket
Подключение
WebSocket инициализируется в AdminComponent при загрузке:
@Component({ ... })
export class AdminComponent implements OnInit {
private readonly websocketsService = inject(WebsocketsService);
ngOnInit() {
this.websocketsService.connect();
}
}WebsocketsService
Использует socket.io-client для подключения к backend. Все входящие события пробрасываются в EventsService.
@Injectable({ providedIn: "root" })
export class WebsocketsService {
private socket?: Socket;
private readonly eventsService = inject(EventsService);
private readonly envService = inject(EnvService);
connect() {
this.socket = io(this.envService.wssUrl(), {
auth: { admin: true }
});
this.socket.on("event", (event: AppEvent) => {
this.eventsService.emit(event.type, event.payload);
});
}
}EventsService
Центральная шина событий. Компоненты подписываются на конкретные типы событий:
@Injectable({ providedIn: "root" })
export class EventsService {
private readonly eventsSubject = new Subject<AppEvent>();
readonly events$ = this.eventsSubject.asObservable();
emit(type: EventsEnum, payload?: unknown) { ... }
on(...events: (EventsEnum | EventsEnum[])[]) {
const flattened = new Set(events.flat());
return this.events$.pipe(filter(event => flattened.has(event.type)));
}
}Конфигурация
wssUrl настраивается через Doppler (или выводится из API_URL):
| Окружение | wssUrl |
|---|---|
| local | http://localhost:3000 |
| dev | https://api.dev.happ.tools |
| prod | https://api.happ.tools |
messages/last
Эндпоинт для массовой загрузки последнего сообщения каждого чата.
API
GET /admin/messages/last?chatIds=id1,id2,id3Response:
{
"chatId1": { "id": "...", "text": "encrypted...", "createdAt": "..." },
"chatId2": { "id": "...", "text": "encrypted...", "createdAt": "..." }
}Использование в ChatsComponent
После загрузки списка чатов, автоматически подтягиваются последние сообщения:
private loadChats() {
this.chatsService.getAll({ page, limit })
.pipe(
tap(response => {
this.chats.set(response.data);
this.paginationMeta.set(response);
}),
map(response => response.data.map(c => c.id)),
switchMap(chatIds =>
chatIds.length > 0
? this.messagesService.getLastMessages(chatIds)
: of({})
)
)
.subscribe(messages => {
this.lastMessages.set(messages);
});
}Последнее сообщение отображается как badge в сайдбаре (текст расшифровывается через CryptoService).
Real-time обновление
При получении MESSAGE_CREATED через WebSocket, lastMessages обновляется без перезагрузки:
this.eventsService
.on(EventsEnum.MESSAGE_CREATED)
.pipe(untilDestroyed(this))
.subscribe(event => {
const payload = event.payload as { chatId: string } & IMessage;
this.lastMessages.update(current => ({
...current,
[payload.chatId]: payload
}));
});Пагинация
Список чатов
- Размер страницы: 20
- Параметры:
page(1-indexed),limit,search,companyId,messenger - Компонент:
SidebarListComponentсMatPaginator - Пагинатор прижат к низу сайдбара
Сообщения в чате
- Размер страницы: 50
- Параметры:
page,limit,search,chatId - Компонент:
MatPaginatorпод областью сообщений
Страницы
Список чатов
URL: /admin/chats
Explorer layout с сайдбаром (список чатов) и основной областью (детали чата).
Фильтры:
- Поиск по имени
- Фильтр по компании
- Фильтр по мессенджеру (
telegram,instagram,messenger,whatsapp,viber-echat)
Детали чата
URL: /admin/chats/:id
Отображает:
- Заголовок: имя контакта, компания, статус AI/Manual, ассистент
- Системные сообщения (кнопка с badge — открывает диалог)
- Сообщения: Text (баблы), FunctionCall/FunctionCallOutput (карточки tool call)
- Контексты: кликабельные чипы для связывания сообщений по
contextId - Токены: input/output/total на каждом сообщении ассистента
Интерфейсы
interface IChat {
id: string;
companyId: string;
assistantId?: string;
messenger: TMessenger;
fromName?: string;
fromId?: string;
toName?: string;
toId?: string;
displayName?: string;
isUnderAiControl: boolean;
createdAt: string;
updatedAt: string;
}
interface IMessage {
id: string;
chatId: string;
fromId?: string;
toId?: string;
text: string;
type: TMessageType; // "Text" | "Photo" | "FunctionCall" | ...
role: TMessageRole; // "User" | "Assistant" | "System" | "Developer"
functionCallId?: string;
functionCallName?: string;
functionCallInput?: Record<string, unknown>;
functionCallOutput?: unknown;
contextId?: string;
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
createdAt: string;
updatedAt: string;
}API Endpoints
| Метод | Endpoint | Описание |
|---|---|---|
| GET | /admin/chats | Список чатов с пагинацией |
| GET | /admin/chats/:id | Детали чата |
| DELETE | /admin/chats/:id | Удалить чат |
| GET | /admin/messages | Сообщения чата (params: chatId, page, limit) |
| GET | /admin/messages/last | Последние сообщения (params: chatIds) |
Layout
Все explorer-layout страницы ограничены высотой 100vh - padding:
.entity-page {
display: flex;
flex-direction: column;
height: calc(100vh - var(--spacing-md) * 2);
}Для корректной работы скролла в flex/grid цепочке, миксины explorerLayout, explorerSidebar, explorerMainContent включают min-height: 0 и overflow: hidden.
Скроллятся только:
.items-listв сайдбаре (пагинатор прижат снизу).messages-areaв деталях чата (хедер и пагинатор фиксированы)