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>,
|
swipes: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
current_swipe: usize,
|
current_swipe: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
timestamp: i64, // Unix timestamp in milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
fn new_user(content: String) -> Self {
|
fn new_user(content: String) -> Self {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
role: "user".to_string(),
|
role: "user".to_string(),
|
||||||
content: content.clone(),
|
content: content.clone(),
|
||||||
swipes: vec![content],
|
swipes: vec![content],
|
||||||
current_swipe: 0,
|
current_swipe: 0,
|
||||||
|
timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_assistant(content: String) -> Self {
|
fn new_assistant(content: String) -> Self {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
content: content.clone(),
|
content: content.clone(),
|
||||||
swipes: vec![content],
|
swipes: vec![content],
|
||||||
current_swipe: 0,
|
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
|
// Auto-resize textarea
|
||||||
function autoResize(textarea) {
|
function autoResize(textarea) {
|
||||||
textarea.style.height = 'auto';
|
textarea.style.height = 'auto';
|
||||||
@@ -80,7 +121,7 @@ function autoResize(textarea) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add message to chat
|
// 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');
|
const messageDiv = document.createElement('div');
|
||||||
messageDiv.className = `message ${isUser ? 'user' : 'assistant'}`;
|
messageDiv.className = `message ${isUser ? 'user' : 'assistant'}`;
|
||||||
|
|
||||||
@@ -105,6 +146,14 @@ function addMessage(content, isUser = false, skipActions = false) {
|
|||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.textContent = content;
|
p.textContent = content;
|
||||||
contentDiv.appendChild(p);
|
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 {
|
} else {
|
||||||
// Assistant messages: render as markdown
|
// Assistant messages: render as markdown
|
||||||
contentDiv.innerHTML = marked.parse(content);
|
contentDiv.innerHTML = marked.parse(content);
|
||||||
@@ -141,6 +190,14 @@ function addMessage(content, isUser = false, skipActions = false) {
|
|||||||
pre.appendChild(copyBtn);
|
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
|
// Build message structure
|
||||||
@@ -566,7 +623,7 @@ function addCopyButtonToCode(block) {
|
|||||||
// Extract message sending logic into separate function
|
// Extract message sending logic into separate function
|
||||||
async function sendMessage(message, isRegenerate = false) {
|
async function sendMessage(message, isRegenerate = false) {
|
||||||
if (!isRegenerate) {
|
if (!isRegenerate) {
|
||||||
addMessage(message, true);
|
addMessage(message, true, false, Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
sendBtn.disabled = true;
|
sendBtn.disabled = true;
|
||||||
@@ -1138,7 +1195,7 @@ async function loadChatHistory() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
history.forEach((msg, index) => {
|
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
|
// Update swipe controls for assistant messages with swipe info
|
||||||
if (msg.role === 'assistant' && messageDiv && msg.swipes && msg.swipes.length > 0) {
|
if (msg.role === 'assistant' && messageDiv && msg.swipes && msg.swipes.length > 0) {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ body {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-circle-large {
|
.avatar-circle-large {
|
||||||
@@ -287,6 +288,24 @@ body {
|
|||||||
margin-top: 12px;
|
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 action buttons */
|
||||||
.message-actions {
|
.message-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user