From 4694114ff97fb942be78cc240d0db7e657b7aef5 Mon Sep 17 00:00:00 2001 From: matt Date: Tue, 14 Oct 2025 12:07:19 -0700 Subject: [PATCH] feat: add message timestamps with smart formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added timestamp field to Message struct in Rust backend - Timestamps automatically captured on message creation - Smart relative time formatting (Just now, Xm ago, time, date) - Timestamps display below message content with subtle styling - Fixed avatar squishing issue with flex-shrink: 0 - Backward compatible with existing messages via serde(default) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src-tauri/src/lib.rs | 14 ++++++++++ src/main.js | 63 +++++++++++++++++++++++++++++++++++++++++--- src/styles.css | 19 +++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7a1a1da..5a90eb8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -174,24 +174,38 @@ struct Message { swipes: Vec, #[serde(default)] current_swipe: usize, + #[serde(default)] + timestamp: i64, // Unix timestamp in milliseconds } impl Message { fn new_user(content: String) -> Self { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + Self { role: "user".to_string(), content: content.clone(), swipes: vec![content], current_swipe: 0, + timestamp, } } fn new_assistant(content: String) -> Self { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + Self { role: "assistant".to_string(), content: content.clone(), swipes: vec![content], current_swipe: 0, + timestamp, } } diff --git a/src/main.js b/src/main.js index 37a72eb..a8fc5e5 100644 --- a/src/main.js +++ b/src/main.js @@ -73,6 +73,47 @@ function makeAvatarClickable(avatarElement, avatarUrl) { }); } +// Format timestamp for display +function formatTimestamp(timestamp) { + if (!timestamp) return ''; + + const date = new Date(timestamp); + const now = new Date(); + const diff = now - date; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + // Just now (less than 1 minute) + if (seconds < 60) { + return 'Just now'; + } + + // Minutes ago (less than 1 hour) + if (minutes < 60) { + return `${minutes}m ago`; + } + + // Today (show time) + if (days === 0) { + return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); + } + + // Yesterday + if (days === 1) { + return `Yesterday at ${date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })}`; + } + + // This week (show day name) + if (days < 7) { + return date.toLocaleDateString('en-US', { weekday: 'short', hour: 'numeric', minute: '2-digit', hour12: true }); + } + + // Older (show date) + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }); +} + // Auto-resize textarea function autoResize(textarea) { textarea.style.height = 'auto'; @@ -80,7 +121,7 @@ function autoResize(textarea) { } // Add message to chat -function addMessage(content, isUser = false, skipActions = false) { +function addMessage(content, isUser = false, skipActions = false, timestamp = null) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${isUser ? 'user' : 'assistant'}`; @@ -105,6 +146,14 @@ function addMessage(content, isUser = false, skipActions = false) { const p = document.createElement('p'); p.textContent = content; contentDiv.appendChild(p); + + // Add timestamp if provided + if (timestamp) { + const timestampDiv = document.createElement('div'); + timestampDiv.className = 'message-timestamp'; + timestampDiv.textContent = formatTimestamp(timestamp); + contentDiv.appendChild(timestampDiv); + } } else { // Assistant messages: render as markdown contentDiv.innerHTML = marked.parse(content); @@ -141,6 +190,14 @@ function addMessage(content, isUser = false, skipActions = false) { pre.appendChild(copyBtn); } }); + + // Add timestamp if provided + if (timestamp) { + const timestampDiv = document.createElement('div'); + timestampDiv.className = 'message-timestamp'; + timestampDiv.textContent = formatTimestamp(timestamp); + contentDiv.appendChild(timestampDiv); + } } // Build message structure @@ -566,7 +623,7 @@ function addCopyButtonToCode(block) { // Extract message sending logic into separate function async function sendMessage(message, isRegenerate = false) { if (!isRegenerate) { - addMessage(message, true); + addMessage(message, true, false, Date.now()); } sendBtn.disabled = true; @@ -1138,7 +1195,7 @@ async function loadChatHistory() { } } else { history.forEach((msg, index) => { - const messageDiv = addMessage(msg.content, msg.role === 'user'); + const messageDiv = addMessage(msg.content, msg.role === 'user', false, msg.timestamp); // Update swipe controls for assistant messages with swipe info if (msg.role === 'assistant' && messageDiv && msg.swipes && msg.swipes.length > 0) { diff --git a/src/styles.css b/src/styles.css index 3f136f1..44b8c4b 100644 --- a/src/styles.css +++ b/src/styles.css @@ -86,6 +86,7 @@ body { border: 1px solid var(--border); background-size: cover; background-position: center; + flex-shrink: 0; } .avatar-circle-large { @@ -287,6 +288,24 @@ body { margin-top: 12px; } +/* Message timestamp */ +.message-timestamp { + font-size: 10px; + color: var(--text-secondary); + opacity: 0.6; + margin-top: 4px; + user-select: none; +} + +.message.user .message-timestamp { + text-align: right; +} + +.message.assistant .message-timestamp { + text-align: left; + margin-left: 4px; +} + /* Message action buttons */ .message-actions { position: absolute;