Smooth WebUI streaming with state-driven viewport motion (#4696)

This commit is contained in:
chengyongru
2026-07-26 00:18:24 +08:00
committed by GitHub
parent 9a7debcb48
commit b0ef759e2c
28 changed files with 3202 additions and 670 deletions
+36
View File
@@ -71,6 +71,7 @@ const RELATIVE_THRESHOLDS: [number, Intl.RelativeTimeFormatUnit][] = [
const relativeTimeFormatters = new Map<string, Intl.RelativeTimeFormat>();
const dateTimeFormatters = new Map<string, Intl.DateTimeFormat>();
const clockTimeFormatters = new Map<string, Intl.DateTimeFormat>();
function activeLocale(locale?: string): string {
return locale || i18n.resolvedLanguage || i18n.language || currentLocale();
@@ -95,6 +96,25 @@ function dateTimeFormatter(locale: string): Intl.DateTimeFormat {
return formatter;
}
function clockTimeFormatter(locale: string): Intl.DateTimeFormat {
const existing = clockTimeFormatters.get(locale);
if (existing) return existing;
const formatter = new Intl.DateTimeFormat(locale, {
hour: "2-digit",
minute: "2-digit",
});
clockTimeFormatters.set(locale, formatter);
return formatter;
}
function isSameLocalCalendarDay(left: Date, right: Date): boolean {
return (
left.getFullYear() === right.getFullYear()
&& left.getMonth() === right.getMonth()
&& left.getDate() === right.getDate()
);
}
export function relativeTime(
value: string | number | null | undefined,
locale?: string,
@@ -120,6 +140,22 @@ export function fmtDateTime(
return date ? dateTimeFormatter(activeLocale(locale)).format(date) : "";
}
/**
* Format a completion timestamp in the browser's local timezone.
* Today's messages stay compact; older messages include their date for orientation.
*/
export function formatMessageEndTime(
value: number | null | undefined,
locale?: string,
): string {
const date = parseDate(value);
if (!date) return "";
const loc = activeLocale(locale);
return isSameLocalCalendarDay(date, new Date())
? clockTimeFormatter(loc).format(date)
: dateTimeFormatter(loc).format(date);
}
/** Human-readable turn duration (wall-clock), locale-aware via ``Intl`` (seconds/minutes). */
export function formatTurnLatency(ms: number, locale?: string): string {
const loc = activeLocale(locale);
+40 -1
View File
@@ -24,6 +24,44 @@ export function normalizeLegacyLongTaskMessages(messages: UIMessage[]): UIMessag
});
}
/**
* Replay timestamps an assistant row when its first output is recorded, while
* latency covers the whole turn. Derive the end from the matching user start
* so the displayed time cannot double-count the pre-output interval.
*/
function deriveAssistantCompletionTimes(messages: UIMessage[]): UIMessage[] {
const userStartedAtByTurn = new Map<string, number>();
let latestUserStartedAt: number | undefined;
return messages.map((message) => {
if (message.role === "user") {
if (Number.isFinite(message.createdAt)) {
latestUserStartedAt = message.createdAt;
if (message.turnId) userStartedAtByTurn.set(message.turnId, message.createdAt);
}
return message;
}
if (
message.role !== "assistant"
|| message.kind === "trace"
|| message.completedAt !== undefined
|| message.latencyMs === undefined
|| !Number.isFinite(message.latencyMs)
|| message.latencyMs < 0
) {
return message;
}
const startedAt = message.turnId
? userStartedAtByTurn.get(message.turnId)
: message.source
? undefined
: latestUserStartedAt;
if (startedAt === undefined) return message;
return { ...message, completedAt: startedAt + message.latencyMs };
});
}
export function projectWebuiThreadMessages(messages: UIMessage[]): UIMessage[] {
const normalized = scrubSubagentUiMessages(normalizeLegacyLongTaskMessages(messages));
const hiddenTurns = new Set(normalized.flatMap((message) => (
@@ -31,10 +69,11 @@ export function projectWebuiThreadMessages(messages: UIMessage[]): UIMessage[] {
? [message.turnId]
: []
)));
return normalized.filter((message) => (
const visible = normalized.filter((message) => (
!isSystemCommandTurnId(message.turnId)
&& (!message.turnId || !hiddenTurns.has(message.turnId))
&& !(message.role === "user" && isModelCommandText(message.content))
&& !(message.role === "assistant" && isModelCommandResponseText(message.content))
));
return deriveAssistantCompletionTimes(visible);
}
+2
View File
@@ -68,6 +68,8 @@ export interface UIMessage {
reasoningStreaming?: boolean;
/** End-to-end wall time for this assistant turn (persisted ``latency_ms`` / ``turn_end``). */
latencyMs?: number;
/** Client epoch milliseconds when the definitive ``turn_end`` was received. */
completedAt?: number;
/** Lightweight provenance for proactive assistant messages. */
source?: UIMessageSource;
/** Stable protocol metadata for grouping all activity emitted by one user turn. */