feat: Enrich in-progress stage display and memoize pipeline page
In-progress stages now show: - Live elapsed time (ticks every second) next to the active stage dot - Run-level token count so far Performance: wrapped StageTimeline, StatusFilter, WorkerStatus, and RecentActivityFeed with React.memo. Memoized filteredVideos with useMemo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e80094dc05
commit
9b2db11095
2 changed files with 59 additions and 17 deletions
|
|
@ -3689,6 +3689,22 @@ a.app-footer__repo:hover {
|
|||
.stage-timeline__step--done + .stage-timeline__step::before {
|
||||
background: #00c853;
|
||||
}
|
||||
.stage-timeline__elapsed {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
margin-left: 6px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stage-timeline__tokens {
|
||||
font-size: 0.65rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
/* ── Recent Activity Feed ─────────────────────────────────────────────────── */
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* expandable event log with token usage and collapsible JSON viewer.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle";
|
||||
import {
|
||||
|
|
@ -45,6 +45,19 @@ function formatTokens(n: number): string {
|
|||
return String(n);
|
||||
}
|
||||
|
||||
|
||||
function formatElapsed(isoStart: string | null): string {
|
||||
if (!isoStart) return "";
|
||||
const ms = Date.now() - new Date(isoStart).getTime();
|
||||
if (ms < 0) return "";
|
||||
const secs = Math.floor(ms / 1000);
|
||||
if (secs < 60) return `${secs}s`;
|
||||
const mins = Math.floor(secs / 60);
|
||||
const remainSecs = secs % 60;
|
||||
if (mins < 60) return `${mins}m ${remainSecs}s`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
return `${hrs}h ${mins % 60}m`;
|
||||
}
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
not_started: "Not Started",
|
||||
queued: "Queued",
|
||||
|
|
@ -343,7 +356,7 @@ function EventLog({ videoId, status, runId }: { videoId: string; status: string;
|
|||
|
||||
// ── Worker Status ────────────────────────────────────────────────────────────
|
||||
|
||||
function WorkerStatus() {
|
||||
const WorkerStatus = React.memo(function WorkerStatus() {
|
||||
const [status, setStatus] = useState<WorkerStatusResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -397,7 +410,7 @@ function WorkerStatus() {
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Debug Mode Toggle ────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -443,7 +456,7 @@ function DebugModeToggle({
|
|||
|
||||
// ── Status Filter ────────────────────────────────────────────────────────────
|
||||
|
||||
function StatusFilter({
|
||||
const StatusFilter = React.memo(function StatusFilter({
|
||||
videos,
|
||||
activeFilter,
|
||||
onFilterChange,
|
||||
|
|
@ -483,7 +496,7 @@ function StatusFilter({
|
|||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Run List ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -614,15 +627,23 @@ const PIPELINE_STAGES = [
|
|||
{ key: "stage6_embed", label: "Embed" },
|
||||
];
|
||||
|
||||
function StageTimeline({ video }: { video: PipelineVideoItem }) {
|
||||
if (video.processing_status !== "processing" && video.processing_status !== "complete" && video.processing_status !== "error") {
|
||||
const StageTimeline = React.memo(function StageTimeline({ video }: { video: PipelineVideoItem }) {
|
||||
const [, setTick] = useState(0);
|
||||
const isProcessing = video.processing_status === "processing";
|
||||
|
||||
// Tick every second while processing to update elapsed time
|
||||
useEffect(() => {
|
||||
if (!isProcessing) return;
|
||||
const id = setInterval(() => setTick((t) => t + 1), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [isProcessing]);
|
||||
|
||||
if (!isProcessing && video.processing_status !== "complete" && video.processing_status !== "error") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeStage = video.active_stage;
|
||||
const activeStatus = video.active_stage_status; // "start" = running, "complete" = done, "error" = failed
|
||||
|
||||
// Determine each stage's state
|
||||
const activeStatus = video.active_stage_status;
|
||||
const activeIdx = PIPELINE_STAGES.findIndex((s) => s.key === activeStage);
|
||||
|
||||
return (
|
||||
|
|
@ -638,7 +659,6 @@ function StageTimeline({ video }: { video: PipelineVideoItem }) {
|
|||
else if (activeStatus === "error") stateClass = "stage-timeline__step--error";
|
||||
}
|
||||
}
|
||||
// If video is complete, all stages are done
|
||||
if (video.processing_status === "complete") {
|
||||
stateClass = "stage-timeline__step--done";
|
||||
}
|
||||
|
|
@ -650,13 +670,19 @@ function StageTimeline({ video }: { video: PipelineVideoItem }) {
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{isProcessing && activeStatus === "start" && (
|
||||
<span className="stage-timeline__elapsed">{formatElapsed(video.stage_started_at)}</span>
|
||||
)}
|
||||
{isProcessing && video.latest_run && video.latest_run.total_tokens > 0 && (
|
||||
<span className="stage-timeline__tokens">{formatTokens(video.latest_run.total_tokens)} tok</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
// ── Recent Activity Feed ─────────────────────────────────────────────────────
|
||||
|
||||
function RecentActivityFeed() {
|
||||
const RecentActivityFeed = React.memo(function RecentActivityFeed() {
|
||||
const [items, setItems] = useState<RecentActivityItem[]>([]);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -718,7 +744,7 @@ function RecentActivityFeed() {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Main Page ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -760,8 +786,8 @@ export default function AdminPipeline() {
|
|||
const videoRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const deepLinked = useRef(false);
|
||||
|
||||
// Compute filtered list (status + creator)
|
||||
const filteredVideos = videos.filter((v) => {
|
||||
// Compute filtered list (status + creator) — memoized to avoid recomputing on unrelated state changes
|
||||
const filteredVideos = useMemo(() => videos.filter((v) => {
|
||||
if (activeFilter !== null && v.processing_status !== activeFilter) return false;
|
||||
if (creatorFilter !== null && v.creator_name !== creatorFilter) return false;
|
||||
if (searchQuery) {
|
||||
|
|
@ -769,7 +795,7 @@ export default function AdminPipeline() {
|
|||
if (!v.filename.toLowerCase().includes(q) && !v.creator_name.toLowerCase().includes(q)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}), [videos, activeFilter, creatorFilter, searchQuery]);
|
||||
|
||||
const load = useCallback(async (isAutoRefresh = false) => {
|
||||
if (!isAutoRefresh) setLoading(true);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue