feat: add message timestamps with smart formatting

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-10-14 12:07:19 -07:00
parent ab6ae14bbc
commit 4694114ff9
3 changed files with 93 additions and 3 deletions

View File

@@ -174,24 +174,38 @@ struct Message {
swipes: Vec<String>,
#[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,
}
}

View File

@@ -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) {

View File

@@ -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;