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 +}