Add timestamps and per-step execution time
Wabifocus committed
Dec 9, 2025 at 19:41 UTC
72439d596613bc4425edf3c1b5158f8f52b487dd
4 files changed
+115
-16
python/helpers/log.py
+4
@@ -1,5 +1,6 @@
1
from dataclasses import dataclass, field
2
import json
3
+import time
4
from typing import Any, Literal, Optional, Dict, TypeVar, TYPE_CHECKING
5
6
T = TypeVar("T")
@@ -131,9 +132,11 @@ class LogItem:
132
kvps: Optional[OrderedDict] = None # Use OrderedDict for kvps
133
id: Optional[str] = None # Add id field
134
guid: str = ""
135
+ timestamp: float = 0.0 # Unix timestamp in seconds
136
137
def __post_init__(self):
138
self.guid = self.log.guid
139
+ self.timestamp = time.time() # Record creation time
140
141
def update(
142
self,
@@ -181,6 +184,7 @@ class LogItem:
184
"content": self.content,
185
"temp": self.temp,
186
"kvps": self.kvps,
187
+ "timestamp": self.timestamp, # Unix timestamp in seconds
188
}
189
190
webui/components/messages/process-group/process-group.css
+46
-10
@@ -6,7 +6,7 @@
6
margin: var(--spacing-sm) 0;
7
padding: var(--spacing-sm) var(--spacing-md);
8
border-radius: var(--border-radius);
9
- background: rgba(31, 60, 30, 0.3);
9
+ background: #1f3c1e;
10
border: 1px solid rgba(255, 255, 255, 0.06);
11
min-width: 200px;
12
max-width: 100%;
@@ -23,7 +23,7 @@
23
margin: 0;
24
border-radius: var(--border-radius) var(--border-radius) 0 0;
25
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
26
- background: rgba(31, 60, 30, 0.4);
26
+ background: transparent;
27
}
28
29
/* Message container with embedded process group */
@@ -87,23 +87,43 @@
87
}
88
89
.process-group-header .group-title {
90
+ flex: 1;
91
font-size: var(--font-size-smaller);
92
font-weight: 400;
93
color: var(--color-text);
94
opacity: 0.7;
95
white-space: nowrap;
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
}
99
100
.process-group-header .step-count {
101
font-size: 0.7rem;
102
color: var(--color-text);
100
- opacity: 0.5;
103
+ opacity: 0.6;
104
flex-shrink: 0;
105
background-color: rgba(255, 255, 255, 0.06);
106
padding: 1px 6px;
107
border-radius: 8px;
108
}
109
110
+.process-group-header .group-timestamp {
111
+ font-size: 0.65rem;
112
+ color: rgba(255, 255, 255, 0.65);
113
+ flex-shrink: 0;
114
+ font-family: var(--font-family-code);
115
+ margin-left: auto;
116
+}
117
+
118
+.process-group-header .group-duration {
119
+ font-size: 0.7rem;
120
+ color: #81c784;
121
+ flex-shrink: 0;
122
+ font-family: var(--font-family-code);
123
+ min-width: 40px;
124
+ text-align: right;
125
+}
126
+
127
/* Process Group Content - Animated expand/collapse */
128
.process-group-content {
129
display: grid;
@@ -163,19 +183,14 @@
183
.process-step[data-type="util"]:hover,
184
.process-step[data-type="info"]:hover,
185
.process-step[data-type="hint"]:hover {
166
- background-color: rgba(120, 115, 100, 0.18);
186
+ background-color: rgba(255, 255, 255, 0.03);
187
}
188
169
-.light-mode .process-step[data-type="util"],
170
-.light-mode .process-step[data-type="info"],
171
-.light-mode .process-step[data-type="hint"] {
172
- background-color: rgba(91, 85, 64, 0.06);
173
-}
189
190
.light-mode .process-step[data-type="util"]:hover,
191
.light-mode .process-step[data-type="info"]:hover,
192
.light-mode .process-step[data-type="hint"]:hover {
178
- background-color: rgba(91, 85, 64, 0.1);
193
+ background-color: rgba(0, 0, 0, 0.04);
194
}
195
196
/* Step Header (clickable) */
@@ -225,6 +240,15 @@
240
text-overflow: ellipsis;
241
}
242
243
+.process-step-header .step-time {
244
+ font-size: 0.65rem;
245
+ color: #81c784;
246
+ flex-shrink: 0;
247
+ font-family: var(--font-family-code);
248
+ min-width: 35px;
249
+ text-align: right;
250
+}
251
+
252
.process-step-header .step-expand-icon {
253
font-size: 0.8rem;
254
opacity: 0.4;
@@ -353,6 +377,18 @@
377
color: #188216;
378
}
379
380
+.light-mode .process-group-header .group-timestamp {
381
+ color: rgba(0, 0, 0, 0.5);
382
+}
383
+
384
+.light-mode .process-group-header .group-duration {
385
+ color: #2e7d32;
386
+}
387
+
388
+.light-mode .process-step-header .step-time {
389
+ color: #388e3c;
390
+}
391
+
392
/* Animation for loading state */
393
@keyframes pulse-step {
394
0%, 100% { opacity: 0.5; }
webui/index.js
+4
-3
@@ -185,8 +185,8 @@ async function updateUserTime() {
185
updateUserTime();
186
setInterval(updateUserTime, 1000);
187
188
-function setMessage(id, type, heading, content, temp, kvps = null) {
189
- const result = msgs.setMessage(id, type, heading, content, temp, kvps);
188
+function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null) {
189
+ const result = msgs.setMessage(id, type, heading, content, temp, kvps, timestamp);
190
const chatHistoryEl = document.getElementById("chat-history");
191
if (preferencesStore.autoScroll && chatHistoryEl) {
192
chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;
@@ -310,7 +310,8 @@ export async function poll() {
310
log.heading,
311
log.content,
312
log.temp,
313
- log.kvps
313
+ log.kvps,
314
+ log.timestamp
315
);
316
}
317
afterMessagesUpdate(response.logs);
webui/js/messages.js
+61
-3
@@ -19,7 +19,7 @@ const MAIN_TYPES = ['user', 'response', 'warning', 'error', 'rate_limit'];
19
20
// Simplified implementation - no complex interactions needed
21
22
-export function setMessage(id, type, heading, content, temp, kvps = null) {
22
+export function setMessage(id, type, heading, content, temp, kvps = null, timestamp = null) {
23
// Check if this is a process type message
24
const isProcessType = PROCESS_TYPES.includes(type);
25
const isMainType = MAIN_TYPES.includes(type);
@@ -49,7 +49,7 @@ export function setMessage(id, type, heading, content, temp, kvps = null) {
49
}
50
51
// Add step to current process group
52
- processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps);
52
+ processStepElement = addProcessStep(currentProcessGroup, id, type, heading, content, kvps, timestamp);
53
return processStepElement;
54
}
55
@@ -1136,6 +1136,7 @@ function createProcessGroup(id) {
1136
<span class="material-symbols-outlined group-icon">neurology</span>
1137
<span class="group-title">Processing...</span>
1138
<span class="step-count">0 steps</span>
1139
+ <span class="group-timestamp"></span>
1140
`;
1141
1142
// Add click handler for expansion
@@ -1164,7 +1165,7 @@ function createProcessGroup(id) {
1165
/**
1166
* Add a step to a process group
1167
*/
1167
-function addProcessStep(group, id, type, heading, content, kvps) {
1168
+function addProcessStep(group, id, type, heading, content, kvps, timestamp = null) {
1169
const groupId = group.getAttribute("data-group-id");
1170
const stepsContainer = group.querySelector(".process-steps");
1171
@@ -1175,6 +1176,21 @@ function addProcessStep(group, id, type, heading, content, kvps) {
1176
step.setAttribute("data-type", type);
1177
step.setAttribute("data-step-id", id);
1178
1179
+ // Store timestamp for duration calculation
1180
+ if (timestamp) {
1181
+ step.setAttribute("data-timestamp", timestamp);
1182
+
1183
+ // Set group start time from first step
1184
+ if (!group.getAttribute("data-start-timestamp")) {
1185
+ group.setAttribute("data-start-timestamp", timestamp);
1186
+ // Update header with formatted datetime
1187
+ const timestampEl = group.querySelector(".group-timestamp");
1188
+ if (timestampEl) {
1189
+ timestampEl.textContent = formatDateTime(timestamp);
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
// Add message-util class for utility/info types (controlled by showUtils preference)
1195
if (type === "util" || type === "info" || type === "hint") {
1196
step.classList.add("message-util");
@@ -1198,10 +1214,22 @@ function addProcessStep(group, id, type, heading, content, kvps) {
1214
// Create step header
1215
const stepHeader = document.createElement("div");
1216
stepHeader.classList.add("process-step-header");
1217
+
1218
+ // Calculate relative time from group start
1219
+ let relativeTimeStr = "";
1220
+ if (timestamp) {
1221
+ const groupStartTime = parseFloat(group.getAttribute("data-start-timestamp") || "0");
1222
+ if (groupStartTime > 0) {
1223
+ const relativeMs = (timestamp - groupStartTime) * 1000;
1224
+ relativeTimeStr = formatRelativeTime(relativeMs);
1225
+ }
1226
+ }
1227
+
1228
stepHeader.innerHTML = `
1229
<span class="material-symbols-outlined step-icon">${icon}</span>
1230
<span class="step-type">${label}</span>
1231
<span class="step-title">${escapeHTML(title)}</span>
1232
+ ${relativeTimeStr ? `<span class="step-time">${relativeTimeStr}</span>` : ""}
1233
<span class="material-symbols-outlined step-expand-icon">expand_more</span>
1234
`;
1235
@@ -1426,3 +1454,33 @@ export function resetProcessGroups() {
1454
currentProcessGroup = null;
1455
messageGroup = null;
1456
}
1457
+
1458
+/**
1459
+ * Format Unix timestamp as date-time string (YYYY-MM-DD HH:MM:SS)
1460
+ */
1461
+function formatDateTime(timestamp) {
1462
+ const date = new Date(timestamp * 1000); // Convert seconds to milliseconds
1463
+ const year = date.getFullYear();
1464
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1465
+ const day = String(date.getDate()).padStart(2, "0");
1466
+ const hours = String(date.getHours()).padStart(2, "0");
1467
+ const minutes = String(date.getMinutes()).padStart(2, "0");
1468
+ const seconds = String(date.getSeconds()).padStart(2, "0");
1469
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
1470
+}
1471
+
1472
+/**
1473
+ * Format relative time for steps (e.g., +0.5s, +2.3s)
1474
+ */
1475
+function formatRelativeTime(ms) {
1476
+ if (ms < 100) {
1477
+ return "+0s";
1478
+ }
1479
+ const seconds = ms / 1000;
1480
+ if (seconds < 60) {
1481
+ return `+${seconds.toFixed(1)}s`;
1482
+ }
1483
+ const minutes = Math.floor(seconds / 60);
1484
+ const remainingSeconds = Math.round(seconds % 60);
1485
+ return `+${minutes}m${remainingSeconds}s`;
1486
+}