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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
63
src/main.js
63
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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user