feat(ui): gemini-style top-anchored scroll, centered chat layout and task drawer sync (closes #49)
This commit is contained in:
@@ -229,13 +229,53 @@ async function saveManualPromptDraft(btnEl) {
|
||||
const highlightedHtml = buildHighlightedPromptHtml(newText, baseline);
|
||||
previewTextEl.innerHTML = highlightedHtml;
|
||||
}
|
||||
|
||||
let actionButtonsContainer = cardContainer.querySelector(".action-buttons-container");
|
||||
if (actionButtonsContainer) {
|
||||
actionButtonsContainer.innerHTML = `
|
||||
<button type="button" onclick="handleActionButtonClick('подтверждаю')"
|
||||
class="bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white shadow-sm font-semibold px-3 py-1.5 rounded-xl text-xs flex items-center gap-1.5 transition cursor-pointer">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
<span>Подтвердить</span>
|
||||
</button>
|
||||
<button type="button" onclick="handleActionButtonClick('отмена')"
|
||||
class="bg-rose-50 hover:bg-rose-100 active:bg-rose-200 text-rose-700 border border-rose-300 font-semibold px-3 py-1.5 rounded-xl text-xs flex items-center gap-1.5 transition cursor-pointer">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
<span>Отменить</span>
|
||||
</button>
|
||||
<button type="button" onclick="openInlinePromptEditor(this)"
|
||||
class="bg-indigo-50 hover:bg-indigo-100 active:bg-indigo-200 text-indigo-700 border border-indigo-300 font-semibold px-3 py-1.5 rounded-xl text-xs flex items-center gap-1.5 transition cursor-pointer">
|
||||
<i class="fa-solid fa-pen-to-square text-xs"></i>
|
||||
<span>✏️ Редактировать</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Ошибка сохранения черновика: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- [CHAT SEND PIPELINE] ---
|
||||
// ⭐️ Абсолютный расчет позиции скролла к верху видимой области
|
||||
function scrollToTurnTop(anchorId) {
|
||||
const chatWindow = document.getElementById("chat-window");
|
||||
const anchorEl = document.getElementById(anchorId);
|
||||
if (!chatWindow || !anchorEl) return;
|
||||
|
||||
const windowRect = chatWindow.getBoundingClientRect();
|
||||
const anchorRect = anchorEl.getBoundingClientRect();
|
||||
const currentScroll = chatWindow.scrollTop;
|
||||
|
||||
// Новая позиция скролла = текущий сдвиг + расстояние от верхней кромки окна до элемента
|
||||
const targetTop = currentScroll + (anchorRect.top - windowRect.top) - 12;
|
||||
|
||||
chatWindow.scrollTo({
|
||||
top: Math.max(0, targetTop),
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
|
||||
// --- [CHAT SEND PIPELINE (GEMINI STYLE TOP-SCROLL)] ---
|
||||
async function sendMessage(e) {
|
||||
if (e && e.preventDefault) e.preventDefault();
|
||||
|
||||
@@ -257,18 +297,29 @@ async function sendMessage(e) {
|
||||
</div>` + userDisplayHtml;
|
||||
}
|
||||
|
||||
const turnId = "turn-" + Date.now();
|
||||
|
||||
const userMsgHtml = `
|
||||
<div class="flex justify-end mb-3">
|
||||
<div class="bg-indigo-600 text-white rounded-2xl px-4 py-2.5 max-w-2xl text-xs sm:text-sm shadow-sm">
|
||||
<div id="${turnId}" class="chat-turn-anchor flex justify-end mb-3 pt-2 scroll-mt-3">
|
||||
<div class="bg-indigo-600 text-white rounded-2xl px-4 py-2.5 max-w-xl text-xs sm:text-sm shadow-sm leading-relaxed">
|
||||
${userDisplayHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
chatWindow.insertAdjacentHTML("beforeend", userMsgHtml);
|
||||
|
||||
// Вставляем вопрос строго перед невидимой распоркой
|
||||
const spacer = document.getElementById("chat-bottom-spacer");
|
||||
if (spacer) {
|
||||
spacer.insertAdjacentHTML("beforebegin", userMsgHtml);
|
||||
} else {
|
||||
chatWindow.insertAdjacentHTML("beforeend", userMsgHtml);
|
||||
}
|
||||
|
||||
input.value = "";
|
||||
updateInputHeight(input);
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
|
||||
// Скроллим сразу при отправке вопроса
|
||||
setTimeout(() => scrollToTurnTop(turnId), 50);
|
||||
|
||||
if (sendBtn) {
|
||||
sendBtn.disabled = true;
|
||||
@@ -384,7 +435,6 @@ async function sendMessage(e) {
|
||||
snapshotsWidgetHtml = renderSnapshotsCard(actionData.data);
|
||||
}
|
||||
|
||||
// ⭐️ БЛОК КАРТОЧКИ СКАЧИВАНИЯ ФАЙЛА
|
||||
let fileDownloadHtml = "";
|
||||
if (actionData && actionData.type === "FILE_DOWNLOAD_CARD") {
|
||||
fileDownloadHtml = `
|
||||
@@ -409,20 +459,28 @@ async function sendMessage(e) {
|
||||
const maxWidthClass = isWideWidget ? "max-w-4xl w-full" : "max-w-2xl";
|
||||
|
||||
const botMsgHtml = `
|
||||
<div class="chat-message-card bg-white border border-slate-200 rounded-2xl p-4 shadow-sm ${maxWidthClass} mb-3" data-original-baseline="${escapeHtml(baselineText)}">
|
||||
<p class="text-[11px] font-bold text-indigo-600 uppercase tracking-wider mb-1">
|
||||
<i class="fa-solid fa-robot mr-1"></i> ${assistantTitle}
|
||||
</p>
|
||||
<div class="prompt-preview-diff-view text-slate-800 text-xs sm:text-sm whitespace-pre-wrap leading-relaxed">${replyText}</div>
|
||||
${previewEditorHtml}
|
||||
${interactiveWidgetHtml}
|
||||
${snapshotsWidgetHtml}
|
||||
${fileDownloadHtml}
|
||||
${actionButtonsHtml}
|
||||
</div>
|
||||
`;
|
||||
chatWindow.insertAdjacentHTML("beforeend", botMsgHtml);
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
<div class="chat-message-card bg-white border border-slate-200 rounded-2xl p-4 shadow-sm w-full mb-3" data-original-baseline="${escapeHtml(baselineText)}">
|
||||
<p class="text-[11px] font-bold text-indigo-600 uppercase tracking-wider mb-1">
|
||||
<i class="fa-solid fa-robot mr-1"></i> ${assistantTitle}
|
||||
</p>
|
||||
<div class="prompt-preview-diff-view text-slate-800 text-xs sm:text-sm whitespace-pre-wrap leading-relaxed">${replyText}</div>
|
||||
${previewEditorHtml}
|
||||
${interactiveWidgetHtml}
|
||||
${snapshotsWidgetHtml}
|
||||
${fileDownloadHtml}
|
||||
${actionButtonsHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Вставляем карточку ответа строго перед невидимой распоркой
|
||||
if (spacer) {
|
||||
spacer.insertAdjacentHTML("beforebegin", botMsgHtml);
|
||||
} else {
|
||||
chatWindow.insertAdjacentHTML("beforeend", botMsgHtml);
|
||||
}
|
||||
|
||||
// Скроллим к началу хода после рендера карточки ответа
|
||||
setTimeout(() => scrollToTurnTop(turnId), 100);
|
||||
|
||||
clearAttachedFile();
|
||||
|
||||
@@ -437,8 +495,12 @@ async function sendMessage(e) {
|
||||
Ошибка связи с сервером.
|
||||
</div>
|
||||
`;
|
||||
chatWindow.insertAdjacentHTML("beforeend", errorHtml);
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
if (spacer) {
|
||||
spacer.insertAdjacentHTML("beforebegin", errorHtml);
|
||||
} else {
|
||||
chatWindow.insertAdjacentHTML("beforeend", errorHtml);
|
||||
}
|
||||
setTimeout(() => scrollToTurnTop(turnId), 50);
|
||||
setInputLocked(false);
|
||||
} finally {
|
||||
const inputEl = document.getElementById("user-input");
|
||||
|
||||
Reference in New Issue
Block a user