From 21b96ee0cf1c8d504eae7f2c8f9dc5393ab24ffe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 22:42:31 +0000 Subject: [PATCH 1/4] feat(chat): move message action buttons to footer, add relative timestamp Buttons (copy, source toggle) moved from top to bottom of each message bubble. Timestamp shown left-aligned in the footer: relative time (< 24h) or short date. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0197sTYR9EjMxRR9Q2m46cbL --- .../src/components/chatPanel/ChatWindow.jsx | 2 +- frontend/src/components/chatPanel/Message.jsx | 46 +++++++++++++------ .../src/components/chatPanel/MessageList.jsx | 1 + .../components/chatPanel/styles/toolbar.css | 27 +++++++---- 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/chatPanel/ChatWindow.jsx b/frontend/src/components/chatPanel/ChatWindow.jsx index ef2c7b6..95e80f2 100644 --- a/frontend/src/components/chatPanel/ChatWindow.jsx +++ b/frontend/src/components/chatPanel/ChatWindow.jsx @@ -68,7 +68,7 @@ const transformPage = (rawMsgs) => { continue; // преамбулу как сообщение не рендерим } if (type !== 'user') sawAi = true; - bubbles.push({ text: m.content, sender: type === 'user' ? 'user' : 'ai' }); + bubbles.push({ text: m.content, sender: type === 'user' ? 'user' : 'ai', timestamp: m.createdAt || null }); } return { bubbles, leadingMetas }; }; diff --git a/frontend/src/components/chatPanel/Message.jsx b/frontend/src/components/chatPanel/Message.jsx index 3659d8f..1200192 100644 --- a/frontend/src/components/chatPanel/Message.jsx +++ b/frontend/src/components/chatPanel/Message.jsx @@ -404,26 +404,32 @@ function getMarkdownComponents(onNavigateToDoc) { }; } -const Message = ({ text, sender, toolCalls, onNavigateToDoc }) => { +/** Форматирует timestamp: если < 24ч — относительное время, иначе — дата. */ +const formatTimestamp = (ts) => { + if (!ts) return null; + const date = new Date(ts); + if (isNaN(date)) return null; + const diffMs = Date.now() - date.getTime(); + const diffMin = Math.floor(diffMs / 60000); + if (diffMs < 0) return null; + if (diffMin < 1) return '< 1 мин.'; + if (diffMin < 60) return `${diffMin} мин. назад`; + const diffH = Math.floor(diffMin / 60); + if (diffH < 24) return `${diffH} ч. назад`; + return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }); +}; + +const Message = ({ text, sender, toolCalls, onNavigateToDoc, timestamp }) => { const { t } = useTranslation('chat'); const [showSource, setShowSource] = useState(false); const messageClass = `message ${sender}`; const hasToolCalls = toolCalls && toolCalls.length > 0; + const timeLabel = formatTimestamp(timestamp); const messageContent = (
{sender === 'ai' ? ( <> -
- - -
{showSource ? (
{text}
) : ( @@ -433,13 +439,27 @@ const Message = ({ text, sender, toolCalls, onNavigateToDoc }) => {
)} +
+ {timeLabel && {timeLabel}} +
+ + +
+
) : ( <> -
+
{text}
+
+ {timeLabel && {timeLabel}}
-
{text}
)}
diff --git a/frontend/src/components/chatPanel/MessageList.jsx b/frontend/src/components/chatPanel/MessageList.jsx index 979f1ec..b13c624 100644 --- a/frontend/src/components/chatPanel/MessageList.jsx +++ b/frontend/src/components/chatPanel/MessageList.jsx @@ -128,6 +128,7 @@ const MessageList = ({ messages, onNavigateToDoc, onLoadMore, hasMore = false, c text={msg.text} sender={msg.sender} toolCalls={msg.toolCalls} + timestamp={msg.timestamp} onNavigateToDoc={onNavigateToDoc} /> ))} diff --git a/frontend/src/components/chatPanel/styles/toolbar.css b/frontend/src/components/chatPanel/styles/toolbar.css index 2feaa67..7b65b54 100644 --- a/frontend/src/components/chatPanel/styles/toolbar.css +++ b/frontend/src/components/chatPanel/styles/toolbar.css @@ -1,20 +1,27 @@ -/* ── Source toggle ── */ -.message-source-toggle { +/* ── Футер сообщения: время слева, кнопки справа ── */ +.message-footer { display: flex; align-items: center; + justify-content: space-between; + gap: 6px; + margin-top: 6px; +} +.message-footer--user { justify-content: flex-end; - gap: 8px; - margin-bottom: 4px; } - -/* Тулбар сверху сообщения (кнопка копирования) */ -.message-toolbar { +.message-footer-actions { display: flex; align-items: center; - margin-bottom: 4px; + gap: 6px; } -.message-toolbar--user { - justify-content: flex-end; +.message-footer-time { + font-size: 0.68rem; + color: #aaa; + white-space: nowrap; + user-select: none; +} +.message.user .message-footer-time { + color: rgba(255, 255, 255, 0.65); } /* ── Кнопка «копировать всё сообщение» ── */ From f6f568540625052ecb27a58ef8e5c070953b97bb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 10:30:12 +0000 Subject: [PATCH 2/4] fix(chat): restore CSS for message-block wrapper after rebase Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0197sTYR9EjMxRR9Q2m46cbL --- .../components/chatPanel/styles/bubbles.css | 20 ++++++++++++++----- .../chatPanel/styles/tool-calls.css | 9 ++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/chatPanel/styles/bubbles.css b/frontend/src/components/chatPanel/styles/bubbles.css index 039239c..439556a 100644 --- a/frontend/src/components/chatPanel/styles/bubbles.css +++ b/frontend/src/components/chatPanel/styles/bubbles.css @@ -1,19 +1,30 @@ +/* Обёртка пузырь + футер — управляет выравниванием в колонке */ +.message-block { + display: flex; + flex-direction: column; + max-width: 75%; + margin-bottom: 10px; +} +.message-block--ai { + align-self: flex-start; +} +.message-block--user { + align-self: flex-end; + margin-left: auto; +} + /* Базовые стили для любого сообщения */ .message { padding: 0.7rem 1rem; border-radius: 14px; - max-width: 75%; line-height: 1.5; word-wrap: break-word; - margin-bottom: 10px; } /* Сообщение от пользователя */ .message.user { background-color: var(--kb-user-bubble-bg); color: var(--kb-user-bubble-color); - align-self: flex-end; - margin-left: auto; border-bottom-right-radius: 4px; } @@ -26,7 +37,6 @@ .message.ai { background-color: var(--kb-ai-bubble-bg); color: var(--kb-ai-bubble-color); - align-self: flex-start; border-bottom-left-radius: 4px; white-space: normal; } diff --git a/frontend/src/components/chatPanel/styles/tool-calls.css b/frontend/src/components/chatPanel/styles/tool-calls.css index 9aab8d3..52e1ba8 100644 --- a/frontend/src/components/chatPanel/styles/tool-calls.css +++ b/frontend/src/components/chatPanel/styles/tool-calls.css @@ -16,10 +16,13 @@ flex-direction: column; } -/* Пузырь заполняет колонку без вложенного max-width:75% */ -.message-row-with-tools .message-main-col > .message { - align-self: stretch; +/* Блок (пузырь + футер) заполняет колонку без вложенного max-width:75% */ +.message-row-with-tools .message-main-col > .message-block { max-width: 100%; + margin-bottom: 0; +} +.message-row-with-tools .message-main-col > .message-block > .message { + align-self: stretch; } .tool-call-notifications { From b5506a98e39cc9c03dfdcedfc2c8a0fa67c874bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 10:41:12 +0000 Subject: [PATCH 3/4] fix(chat): correct timestamp field, move footer outside bubble with proper alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - transformPage read wrong field (m.createdAt → m.timestamp); backend ChatMessage exposes `timestamp`, so dates were always null. History now shows time/date. - Live bubbles (optimistic user echo, streamed AI) now get a client-side timestamp so the date appears immediately, before reload. - Message render reverted during rebase: footer was back inside the bubble without the .message-block wrapper, breaking width and alignment. Restored footer outside the bubble; AI block left-aligned with buttons on the right, user block right- aligned with buttons on the left. - toolbar.css footer styles updated for the new out-of-bubble placement. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0197sTYR9EjMxRR9Q2m46cbL --- .../src/components/chatPanel/ChatWindow.jsx | 7 +- frontend/src/components/chatPanel/Message.jsx | 79 +++++++++++-------- .../components/chatPanel/chatEventReducer.js | 4 +- .../components/chatPanel/styles/toolbar.css | 30 +++---- 4 files changed, 65 insertions(+), 55 deletions(-) diff --git a/frontend/src/components/chatPanel/ChatWindow.jsx b/frontend/src/components/chatPanel/ChatWindow.jsx index 046ea2c..c676819 100644 --- a/frontend/src/components/chatPanel/ChatWindow.jsx +++ b/frontend/src/components/chatPanel/ChatWindow.jsx @@ -73,7 +73,7 @@ const transformPage = (rawMsgs) => { continue; // преамбулу как сообщение не рендерим } if (type !== 'user') sawAi = true; - bubbles.push({ text: m.content, sender: type === 'user' ? 'user' : 'ai', timestamp: m.createdAt || null }); + bubbles.push({ text: m.content, sender: type === 'user' ? 'user' : 'ai', timestamp: m.timestamp || null }); } return { bubbles, leadingMetas }; }; @@ -523,7 +523,10 @@ const ChatWindow = ({ onNavigateToDoc, isActive = true, activeChatId: propActive setChats((prev) => { const found = prev.find((c) => c.id === activeChatId); if (!found) return prev; - const newMessages = [...(found.messages || []), { text, sender: 'user', clientMsgId }]; + const newMessages = [ + ...(found.messages || []), + { text, sender: 'user', clientMsgId, timestamp: new Date().toISOString() }, + ]; const updatedChat = { ...found, id: conversationId, diff --git a/frontend/src/components/chatPanel/Message.jsx b/frontend/src/components/chatPanel/Message.jsx index abd5e66..7017e43 100644 --- a/frontend/src/components/chatPanel/Message.jsx +++ b/frontend/src/components/chatPanel/Message.jsx @@ -491,50 +491,61 @@ const Message = ({ text, sender, toolCalls, toolCallsRunId, preparing, conversat const showPreparing = preparing && sender === 'ai'; const timeLabel = formatTimestamp(timestamp); - const messageContent = ( + // Пузырь — только контент сообщения, без футера + const bubble = (
{sender === 'ai' ? ( - <> - {showSource ? ( -
{text}
- ) : ( -
- - {text} - -
- )} -
- {timeLabel && {timeLabel}} -
- - -
+ showSource ? ( +
{text}
+ ) : ( +
+ + {text} +
- + ) ) : ( - <> -
{text}
-
- {timeLabel && {timeLabel}} - -
- +
{text}
)}
); + // Футер под пузырём: AI — справа (время слева, кнопки справа), + // user — слева (кнопка + время). + const footer = + sender === 'ai' ? ( +
+ {timeLabel && {timeLabel}} +
+ + +
+
+ ) : ( +
+ + {timeLabel && {timeLabel}} +
+ ); + + const messageBlock = ( +
+ {bubble} + {footer} +
+ ); + if (hasToolCalls && sender === 'ai') { return (
- {messageContent} + {messageBlock} {showPreparing && }
@@ -546,13 +557,13 @@ const Message = ({ text, sender, toolCalls, toolCallsRunId, preparing, conversat if (showPreparing) { return ( <> - {messageContent} + {messageBlock} ); } - return messageContent; + return messageBlock; }; export default Message; diff --git a/frontend/src/components/chatPanel/chatEventReducer.js b/frontend/src/components/chatPanel/chatEventReducer.js index 54fe95e..7479eda 100644 --- a/frontend/src/components/chatPanel/chatEventReducer.js +++ b/frontend/src/components/chatPanel/chatEventReducer.js @@ -53,7 +53,7 @@ const lastAiIndexForRun = (msgs, runId) => { }; const pushAi = (msgs, runId) => { - msgs.push({ text: '', sender: 'ai', runId, toolCalls: [] }); + msgs.push({ text: '', sender: 'ai', runId, toolCalls: [], timestamp: new Date().toISOString() }); return msgs.length - 1; }; @@ -97,7 +97,7 @@ export function applyChatEvent(chat, ev, ctx) { // Дубликат после reload: сообщение уже подгрузилось из БД (хвост истории). const last = msgs[msgs.length - 1]; if (last && last.sender === 'user' && last.text === payload?.text) return chat; - msgs.push({ text: payload?.text || '', sender: 'user' }); + msgs.push({ text: payload?.text || '', sender: 'user', timestamp: new Date().toISOString() }); return { ...chat, messages: msgs }; } diff --git a/frontend/src/components/chatPanel/styles/toolbar.css b/frontend/src/components/chatPanel/styles/toolbar.css index 7b65b54..2ac150f 100644 --- a/frontend/src/components/chatPanel/styles/toolbar.css +++ b/frontend/src/components/chatPanel/styles/toolbar.css @@ -1,28 +1,34 @@ -/* ── Футер сообщения: время слева, кнопки справа ── */ +/* ── Футер под пузырём: время + кнопки ── + Рендерится ВНЕ пузыря (sibling .message), на фоне чата. */ .message-footer { display: flex; align-items: center; - justify-content: space-between; gap: 6px; - margin-top: 6px; + padding: 2px 2px 0; +} + +/* AI: время слева, кнопки справа */ +.message-footer--ai { + justify-content: space-between; } + +/* User: кнопка + время слева (пузырь прижат вправо) */ .message-footer--user { - justify-content: flex-end; + justify-content: flex-start; } + .message-footer-actions { display: flex; align-items: center; gap: 6px; } + .message-footer-time { font-size: 0.68rem; color: #aaa; white-space: nowrap; user-select: none; } -.message.user .message-footer-time { - color: rgba(255, 255, 255, 0.65); -} /* ── Кнопка «копировать всё сообщение» ── */ .message-copy-btn { @@ -48,16 +54,6 @@ color: var(--kb-success); } -/* На синем пузыре пользователя — светлые тона */ -.message.user .message-copy-btn { - color: rgba(255, 255, 255, 0.8); -} -.message.user .message-copy-btn:hover { - color: #fff; - background: rgba(255, 255, 255, 0.18); - border-color: rgba(255, 255, 255, 0.35); -} - /* ── Кнопка переключения источника ── */ .message-source-btn { background: none; From c17fcbba10aa53026a0f0eed1fe07587782f5cb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 10:48:37 +0000 Subject: [PATCH 4/4] feat(chat): user timestamp right-aligned, full datetime on hover User message footer: copy button left, date right (mirror of AI layout). Both AI and user time labels show full date+time on hover via native title tooltip. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0197sTYR9EjMxRR9Q2m46cbL --- frontend/src/components/chatPanel/Message.jsx | 24 +++++++++++++++---- .../components/chatPanel/styles/toolbar.css | 5 ++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/chatPanel/Message.jsx b/frontend/src/components/chatPanel/Message.jsx index 7017e43..8e3302c 100644 --- a/frontend/src/components/chatPanel/Message.jsx +++ b/frontend/src/components/chatPanel/Message.jsx @@ -483,6 +483,13 @@ const formatTimestamp = (ts) => { return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }); }; +const formatFullDatetime = (ts) => { + if (!ts) return null; + const date = new Date(ts); + if (isNaN(date)) return null; + return date.toLocaleString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }); +}; + const Message = ({ text, sender, toolCalls, toolCallsRunId, preparing, conversationId, onNavigateToDoc, timestamp }) => { const { t } = useTranslation('chat'); const [showSource, setShowSource] = useState(false); @@ -490,6 +497,7 @@ const Message = ({ text, sender, toolCalls, toolCallsRunId, preparing, conversat const hasToolCalls = toolCalls && toolCalls.length > 0; const showPreparing = preparing && sender === 'ai'; const timeLabel = formatTimestamp(timestamp); + const timeTitle = formatFullDatetime(timestamp); // Пузырь — только контент сообщения, без футера const bubble = ( @@ -510,12 +518,16 @@ const Message = ({ text, sender, toolCalls, toolCallsRunId, preparing, conversat
); - // Футер под пузырём: AI — справа (время слева, кнопки справа), - // user — слева (кнопка + время). + // Футер под пузырём: AI — время слева, кнопки справа; + // user — кнопка слева, время справа. const footer = sender === 'ai' ? (
- {timeLabel && {timeLabel}} + {timeLabel && ( + + {timeLabel} + + )}
); diff --git a/frontend/src/components/chatPanel/styles/toolbar.css b/frontend/src/components/chatPanel/styles/toolbar.css index 2ac150f..d931ce8 100644 --- a/frontend/src/components/chatPanel/styles/toolbar.css +++ b/frontend/src/components/chatPanel/styles/toolbar.css @@ -12,9 +12,9 @@ justify-content: space-between; } -/* User: кнопка + время слева (пузырь прижат вправо) */ +/* User: кнопка слева, время справа (зеркально относительно AI) */ .message-footer--user { - justify-content: flex-start; + justify-content: space-between; } .message-footer-actions { @@ -28,6 +28,7 @@ color: #aaa; white-space: nowrap; user-select: none; + cursor: default; } /* ── Кнопка «копировать всё сообщение» ── */