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 {
|
.stage-timeline__step--done + .stage-timeline__step::before {
|
||||||
background: #00c853;
|
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 ─────────────────────────────────────────────────── */
|
/* ── Recent Activity Feed ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* expandable event log with token usage and collapsible JSON viewer.
|
* 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 { useSearchParams } from "react-router-dom";
|
||||||
import { useDocumentTitle } from "../hooks/useDocumentTitle";
|
import { useDocumentTitle } from "../hooks/useDocumentTitle";
|
||||||
import {
|
import {
|
||||||
|
|
@ -45,6 +45,19 @@ function formatTokens(n: number): string {
|
||||||
return String(n);
|
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> = {
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
not_started: "Not Started",
|
not_started: "Not Started",
|
||||||
queued: "Queued",
|
queued: "Queued",
|
||||||
|
|
@ -343,7 +356,7 @@ function EventLog({ videoId, status, runId }: { videoId: string; status: string;
|
||||||
|
|
||||||
// ── Worker Status ────────────────────────────────────────────────────────────
|
// ── Worker Status ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function WorkerStatus() {
|
const WorkerStatus = React.memo(function WorkerStatus() {
|
||||||
const [status, setStatus] = useState<WorkerStatusResponse | null>(null);
|
const [status, setStatus] = useState<WorkerStatusResponse | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -397,7 +410,7 @@ function WorkerStatus() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ── Debug Mode Toggle ────────────────────────────────────────────────────────
|
// ── Debug Mode Toggle ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -443,7 +456,7 @@ function DebugModeToggle({
|
||||||
|
|
||||||
// ── Status Filter ────────────────────────────────────────────────────────────
|
// ── Status Filter ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function StatusFilter({
|
const StatusFilter = React.memo(function StatusFilter({
|
||||||
videos,
|
videos,
|
||||||
activeFilter,
|
activeFilter,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
|
|
@ -483,7 +496,7 @@ function StatusFilter({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ── Run List ─────────────────────────────────────────────────────────────────
|
// ── Run List ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -614,15 +627,23 @@ const PIPELINE_STAGES = [
|
||||||
{ key: "stage6_embed", label: "Embed" },
|
{ key: "stage6_embed", label: "Embed" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function StageTimeline({ video }: { video: PipelineVideoItem }) {
|
const StageTimeline = React.memo(function StageTimeline({ video }: { video: PipelineVideoItem }) {
|
||||||
if (video.processing_status !== "processing" && video.processing_status !== "complete" && video.processing_status !== "error") {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeStage = video.active_stage;
|
const activeStage = video.active_stage;
|
||||||
const activeStatus = video.active_stage_status; // "start" = running, "complete" = done, "error" = failed
|
const activeStatus = video.active_stage_status;
|
||||||
|
|
||||||
// Determine each stage's state
|
|
||||||
const activeIdx = PIPELINE_STAGES.findIndex((s) => s.key === activeStage);
|
const activeIdx = PIPELINE_STAGES.findIndex((s) => s.key === activeStage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -638,7 +659,6 @@ function StageTimeline({ video }: { video: PipelineVideoItem }) {
|
||||||
else if (activeStatus === "error") stateClass = "stage-timeline__step--error";
|
else if (activeStatus === "error") stateClass = "stage-timeline__step--error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If video is complete, all stages are done
|
|
||||||
if (video.processing_status === "complete") {
|
if (video.processing_status === "complete") {
|
||||||
stateClass = "stage-timeline__step--done";
|
stateClass = "stage-timeline__step--done";
|
||||||
}
|
}
|
||||||
|
|
@ -650,13 +670,19 @@ function StageTimeline({ video }: { video: PipelineVideoItem }) {
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
|
|
||||||
// ── Recent Activity Feed ─────────────────────────────────────────────────────
|
// ── Recent Activity Feed ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function RecentActivityFeed() {
|
const RecentActivityFeed = React.memo(function RecentActivityFeed() {
|
||||||
const [items, setItems] = useState<RecentActivityItem[]>([]);
|
const [items, setItems] = useState<RecentActivityItem[]>([]);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -718,7 +744,7 @@ function RecentActivityFeed() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
// ── Main Page ────────────────────────────────────────────────────────────────
|
// ── Main Page ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -760,8 +786,8 @@ export default function AdminPipeline() {
|
||||||
const videoRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const videoRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
const deepLinked = useRef(false);
|
const deepLinked = useRef(false);
|
||||||
|
|
||||||
// Compute filtered list (status + creator)
|
// Compute filtered list (status + creator) — memoized to avoid recomputing on unrelated state changes
|
||||||
const filteredVideos = videos.filter((v) => {
|
const filteredVideos = useMemo(() => videos.filter((v) => {
|
||||||
if (activeFilter !== null && v.processing_status !== activeFilter) return false;
|
if (activeFilter !== null && v.processing_status !== activeFilter) return false;
|
||||||
if (creatorFilter !== null && v.creator_name !== creatorFilter) return false;
|
if (creatorFilter !== null && v.creator_name !== creatorFilter) return false;
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
|
|
@ -769,7 +795,7 @@ export default function AdminPipeline() {
|
||||||
if (!v.filename.toLowerCase().includes(q) && !v.creator_name.toLowerCase().includes(q)) return false;
|
if (!v.filename.toLowerCase().includes(q) && !v.creator_name.toLowerCase().includes(q)) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
}), [videos, activeFilter, creatorFilter, searchQuery]);
|
||||||
|
|
||||||
const load = useCallback(async (isAutoRefresh = false) => {
|
const load = useCallback(async (isAutoRefresh = false) => {
|
||||||
if (!isAutoRefresh) setLoading(true);
|
if (!isAutoRefresh) setLoading(true);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue