Skip to content

Чати

Управление чатами между клиентами и 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 при загрузке:

typescript
@Component({ ... })
export class AdminComponent implements OnInit {
	private readonly websocketsService = inject(WebsocketsService);

	ngOnInit() {
		this.websocketsService.connect();
	}
}

WebsocketsService

Использует socket.io-client для подключения к backend. Все входящие события пробрасываются в EventsService.

typescript
@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

Центральная шина событий. Компоненты подписываются на конкретные типы событий:

typescript
@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
localhttp://localhost:3000
devhttps://api.dev.happ.tools
prodhttps://api.happ.tools

messages/last

Эндпоинт для массовой загрузки последнего сообщения каждого чата.

API

GET /admin/messages/last?chatIds=id1,id2,id3

Response:

json
{
	"chatId1": { "id": "...", "text": "encrypted...", "createdAt": "..." },
	"chatId2": { "id": "...", "text": "encrypted...", "createdAt": "..." }
}

Использование в ChatsComponent

После загрузки списка чатов, автоматически подтягиваются последние сообщения:

typescript
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 обновляется без перезагрузки:

typescript
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 на каждом сообщении ассистента

Интерфейсы

typescript
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:

scss
.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 в деталях чата (хедер и пагинатор фиксированы)

SaaS Admin Documentation