diff --git a/cmd/creator-web/static/assets/chat.css b/cmd/creator-web/static/assets/chat.css index 07625df..db45089 100644 --- a/cmd/creator-web/static/assets/chat.css +++ b/cmd/creator-web/static/assets/chat.css @@ -67,14 +67,26 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; .side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; } .chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; } .surface-panel { min-height: 0; position: relative; overflow-y: auto; } -.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; } -.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; } -.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; } -.message.user { align-self: flex-end; background: #151e1a; } -.message.assistant { align-self: flex-start; background: #0f1613; } +.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; background: radial-gradient(circle at 50% 0%, rgba(201, 168, 95, .045), transparent 42%); } +.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: clamp(28px, 5vh, 56px) 18px 24px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; scrollbar-color: #39483f transparent; } +.message { max-width: min(720px, 92%); padding: 13px 16px 14px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; } +.message.user { align-self: flex-end; background: #18221c; border-color: #3a493d; } +.message.assistant { align-self: flex-start; background: #111a15; box-shadow: 0 8px 20px rgba(0, 0, 0, .12); } +.message-head { display: flex; align-items: baseline; gap: 9px; margin-bottom: 8px; } +.message-role { color: var(--signal); font: 600 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; } +.message.user .message-role { color: var(--good); } +.message-time { color: #68776d; font: 9px/1 var(--mono); } +.message-content { white-space: pre-wrap; overflow-wrap: anywhere; } .message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; } -.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); } -.composer button { height: 48px; align-self: end; } +.composer { width: min(100%, 920px); margin: 0 auto; padding: 10px 18px 8px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; border-top: 1px solid rgba(42, 53, 47, .85); background: linear-gradient(var(--base), rgba(11, 16, 14, .98)); } +.composer-input-wrap { min-width: 0; border: 1px solid #3a493f; border-radius: 5px; background: #101813; transition: border-color .15s ease, box-shadow .15s ease; } +.composer-input-wrap:focus-within { border-color: var(--signal); box-shadow: 0 0 0 3px rgba(201, 168, 95, .12); } +.composer textarea { min-height: 48px; max-height: 220px; resize: none; border: 0; background: transparent; } +.composer-hint { display: flex; justify-content: space-between; gap: 12px; padding: 0 12px 8px; color: #68776d; font: 9px/1 var(--mono); } +.composer-hint output { color: var(--muted); white-space: nowrap; } +.composer button { height: 48px; align-self: end; display: inline-flex; align-items: center; justify-content: space-between; gap: 8px; padding-inline: 15px; } +.composer button[aria-busy="true"] { color: #4b432d; background: #ad9353; } +.composer button span:last-child { opacity: .65; font: 16px/1 var(--mono); } .composer button:disabled { cursor: wait; filter: saturate(.3); } .media-tools { width: min(100%, 920px); margin: 0 auto; padding: 0 18px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em; } .media-tools label { display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 8px; border: 1px solid var(--line); color: var(--ink); cursor: pointer; } @@ -107,8 +119,9 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; .empty-state.error { color: var(--bad); } @media (max-width: 640px) { .chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; } - .composer { grid-template-columns: 1fr; } + .composer { grid-template-columns: 1fr; padding-inline: 12px; } .composer button { width: 100%; } + .composer-hint { font-size: 8px; } .conversation-starters { align-items: stretch; } .conversation-starters span { width: 100%; } .chat-header { gap: 10px; padding: 0 12px; } @@ -117,8 +130,12 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px; .chat-header > .operator-link, .build-meta { display: none; } .chat-header > #logout { flex: 0 0 38px; } .chat-layout { height: auto; display: block; } + .chat-layout { display: flex; flex-direction: column; } + .chat-main { order: 1; } .chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; } .chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); } + .chat-left { order: 2; } + .chat-right { order: 3; } .chat-main { height: 70vh; min-height: 520px; } .chat-left { padding-bottom: 20px; } .chat-right { padding-top: 20px; } diff --git a/cmd/creator-web/static/assets/chat.js b/cmd/creator-web/static/assets/chat.js index 0298a26..a9bb14d 100644 --- a/cmd/creator-web/static/assets/chat.js +++ b/cmd/creator-web/static/assets/chat.js @@ -6,6 +6,8 @@ const messages = document.querySelector("#messages"); const chatForm = document.querySelector("#chat-form"); const messageInput = document.querySelector("#message"); const sendButton = document.querySelector("#send"); +const sendLabel = document.querySelector("#send-label"); +const messageCount = document.querySelector("#message-count"); const logoutButton = document.querySelector("#logout"); const activeFocus = document.querySelector("#active-focus"); const cognitiveStatus = document.querySelector("#cognitive-status"); @@ -24,6 +26,13 @@ let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || ""; let localHistory = []; let cognitiveStatusTimer = null; +function resizeComposerInput() { + if (!messageInput) return; + messageInput.style.height = "auto"; + messageInput.style.height = `${Math.min(messageInput.scrollHeight, 220)}px`; + if (messageCount) messageCount.textContent = `${messageInput.value.length.toLocaleString("ru-RU")} / 16 384`; +} + class ChatApiError extends Error { constructor(status, detail = "") { super(detail || `chat api returned ${status}`); @@ -95,6 +104,7 @@ function showChat() { } startCognitiveStatusPolling(); messageInput.focus(); + resizeComposerInput(); } function showLogin() { @@ -106,7 +116,22 @@ function showLogin() { function appendMessage(kind, text, meta = "", media = null) { const node = document.createElement("div"); node.className = `message ${kind}`; - node.textContent = text; + const head = document.createElement("div"); + head.className = "message-head"; + const role = document.createElement("strong"); + role.className = "message-role"; + role.textContent = kind === "user" ? "Вы" : "Домовой"; + const time = document.createElement("time"); + time.className = "message-time"; + const now = new Date(); + time.dateTime = now.toISOString(); + time.textContent = now.toLocaleTimeString("ru-RU", {hour: "2-digit", minute: "2-digit"}); + head.append(role, time); + node.appendChild(head); + const content = document.createElement("div"); + content.className = "message-content"; + content.textContent = text; + node.appendChild(content); if (meta) { const small = document.createElement("small"); small.textContent = meta; @@ -279,6 +304,9 @@ async function sendChatMessage(text, options = {}) { if (!clean) return; if (!options.silentUser) appendMessage("user", clean); sendButton.disabled = true; + sendButton.setAttribute("aria-busy", "true"); + if (sendLabel) sendLabel.textContent = "Домовой обрабатывает"; + messages.setAttribute("aria-busy", "true"); try { const result = await api("/api/chat/messages", {method: "POST", body: JSON.stringify({conversation_id: conversationID, message: clean})}); conversationID = result.conversation_id; @@ -299,6 +327,9 @@ async function sendChatMessage(text, options = {}) { } } finally { sendButton.disabled = false; + sendButton.removeAttribute("aria-busy"); + if (sendLabel) sendLabel.textContent = "Отправить"; + messages.removeAttribute("aria-busy"); messageInput.focus(); } } @@ -334,9 +365,12 @@ chatForm.addEventListener("submit", async (event) => { const text = messageInput.value.trim(); if (!text) return; messageInput.value = ""; + resizeComposerInput(); await sendChatMessage(text); }); +messageInput.addEventListener("input", resizeComposerInput); + async function processMedia(input, path, label) { const file = input?.files?.[0]; if (!file || !mediaStatus) return; @@ -405,3 +439,4 @@ if (token) { } else { showLogin(); } +resizeComposerInput(); diff --git a/cmd/creator-web/static/chat.html b/cmd/creator-web/static/chat.html index e3b1017..f494e27 100644 --- a/cmd/creator-web/static/chat.html +++ b/cmd/creator-web/static/chat.html @@ -57,8 +57,11 @@
- - +
+ +
Enter — отправить · Shift+Enter — новая строка0 / 16384
+
+