Files
WhatIDo/public/chat.html
T
dev 3ac54f57b4 feat(chat): ответы на сообщения с цитатой и переходом к оригиналу
Кнопка «Ответить» у каждого сообщения: цитата показывается в новом
сообщении и в поле ввода, отмена — ✕ или Escape, переключение треда
сбрасывает ответ. Клик по цитате прокручивает ленту к исходному
сообщению и подсвечивает его.

Цитата повторяет вложение оригинала: имя файла с иконкой, для
картинок — миниатюра WebP через существующий /api/chat/files/:token.

- chat_messages.reply_to_id: FK на саму таблицу с ON DELETE SET NULL,
  миграция и ensureChatTables, индекс
- POST .../messages принимает reply_to_id и проверяет, что родитель
  в этом же треде и не удалён; вложения при ошибке подчищаются
- цитата едет в списке, при отправке и в SSE правки/удаления, так что
  у собеседника она обновляется по живому каналу
- бэкап: reply_to_id в normalizeRestoreData, при restore ссылка
  принимается только если родитель восстановился, иначе NULL
- shorts: скрывать поле «Резидент» при добавлении без разбора строк
2026-10-06 00:33:17 +03:00

112 lines
5.6 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Чаты — Админ-панель</title>
<link rel="stylesheet" href="admin.css">
</head>
<body data-page="chat">
<div class="layout">
<div class="sidebar" id="sidebar"></div>
<div class="main">
<div class="page-head">
<h2>Чаты</h2>
<p class="page-sub">Переписка тьюторов с администратором: сообщения, фотографии и документы</p>
<span class="page-status" id="authStatus"><span class="spinner dark"></span> Загрузка…</span>
<button class="head-btn" id="chatReloadBtn" type="button" title="Обновить диалоги"><i data-lucide="rotate-ccw"></i> Обновить</button>
</div>
<div class="chat-layout" id="chatLayout">
<div class="chat-side" id="chatSide">
<div class="chat-side-head">
<span class="chat-side-title">Диалоги</span>
<span class="grow"></span>
<button class="btn-primary ghost chat-side-toggle" id="chatHiddenBtn" type="button" hidden title="Показать скрытые диалоги"><i data-lucide="archive"></i><span class="chat-hidden-count" id="chatHiddenCount" hidden></span></button>
<button class="btn-primary ghost chat-new-btn" id="chatNewBtn" type="button" hidden title="Начать диалог с тьютором"><i data-lucide="user-plus"></i> Начать диалог</button>
</div>
<div class="chat-side-hint" id="chatHiddenHint" hidden>Скрытые диалоги. Новое сообщение от тьютора вернёт диалог в список.</div>
<div class="chat-thread-list" id="chatThreadList"></div>
</div>
<div class="chat-pane" id="chatPane">
<div class="chat-empty" id="chatEmpty">
<i data-lucide="messages-square"></i>
<p class="chat-empty-title">Диалог не выбран</p>
<p class="chat-empty-sub">Выберите тьютора слева или начните новый диалог</p>
</div>
<div class="chat-main" id="chatMain" hidden>
<div class="chat-head">
<span class="chat-avatar" id="chatAvatar"></span>
<div class="chat-head-meta">
<div class="chat-head-name" id="chatPeerName"></div>
<div class="chat-head-sub" id="chatPeerSub"></div>
</div>
<span class="grow"></span>
</div>
<div class="chat-messages" id="chatMessages"></div>
<div class="chat-composer">
<div class="chat-reply-bar" id="chatReplyBar" hidden>
<i data-lucide="corner-up-left"></i>
<span class="chat-reply-meta">
<span class="chat-reply-who" id="chatReplyWho"></span>
<span class="chat-reply-text" id="chatReplyText"></span>
<span class="chat-reply-file" id="chatReplyFile" hidden></span>
</span>
<button type="button" class="chat-attach-del" id="chatReplyCancel" title="Отменить ответ">✕</button>
</div>
<div class="chat-attach-list" id="chatAttachList" hidden></div>
<div class="chat-compose-row">
<button class="chat-attach-btn" id="chatAttachBtn" type="button" title="Прикрепить файл или фото"><i data-lucide="paperclip"></i></button>
<input type="file" id="chatFileInput" multiple hidden>
<textarea class="chat-input" id="chatInput" rows="1" placeholder="Сообщение… (Ctrl+V — вставить фото)" maxlength="5000"></textarea>
<button class="btn-primary" id="chatSendBtn" type="button"><i data-lucide="send"></i> Отправить</button>
</div>
<div class="hint chat-hint" id="chatHint">Можно вставить скриншот или фото прямо из буфера обмена — Ctrl+V. Фото, документы и архивы, до 10 штук.</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="chatNewModal">
<div class="edit-modal chat-new-modal">
<h3>Начать диалог</h3>
<div class="settings-field">
<label>Тьютор</label>
<select id="chatNewUser" class="settings-input"></select>
</div>
<div class="card-foot" style="justify-content:flex-end;gap:8px">
<button type="button" class="btn-primary ghost" id="chatNewCancel">Отмена</button>
<button type="submit" class="btn-primary" id="chatNewOk">Открыть диалог</button>
</div>
</div>
</div>
<div class="modal-overlay" id="chatEditModal">
<div class="edit-modal chat-edit-modal">
<h3>Редактировать сообщение</h3>
<textarea id="chatEditText" class="settings-input" maxlength="5000"></textarea>
<div class="hint">Ctrl+Enter — сохранить. Вложения сообщения останутся как есть.</div>
<div class="card-foot" style="justify-content:flex-end;gap:8px">
<button type="button" class="btn-primary ghost" id="chatEditCancel">Отмена</button>
<button type="submit" class="btn-primary" id="chatEditSave">Сохранить</button>
</div>
</div>
</div>
<div class="modal-overlay" id="imgModal">
<img id="imgModalSrc">
</div>
<div class="toast" id="toast"></div>
<script src="vendor/lucide.min.js"></script>
<script src="js/datetime.js"></script>
<script src="admin.js"></script>
<script src="js/chat.js"></script>
</body>
</html>