From 59b9f6d5f7da623198bfc793d0f00e096b7bdb54 Mon Sep 17 00:00:00 2001 From: forthfate Date: Sat, 26 Sep 2026 10:20:57 +0900 Subject: [PATCH] fix: open persona timeline evidence modal --- .../src/components/ui/evidence-viewer.tsx | 18 +++++++++++- frontend/src/features/improvements/page.tsx | 28 +++++++++++++------ frontend/src/theme-overrides.css | 2 +- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/ui/evidence-viewer.tsx b/frontend/src/components/ui/evidence-viewer.tsx index a1e367b5..f08634c3 100644 --- a/frontend/src/components/ui/evidence-viewer.tsx +++ b/frontend/src/components/ui/evidence-viewer.tsx @@ -14,6 +14,7 @@ export function visualEvidenceArtifacts(files: SavedDataFile[]): EvidenceArtifac return files.reduce((artifacts, file, index) => { const path = file.relative_path ?? file.path; if (!path) return artifacts; + if (artifacts.some((artifact) => artifact.id === path)) return artifacts; const name = file.label || file.filename || `#${index + 1}`; const contentType = file.content_type?.toLowerCase(); const extension = path.toLowerCase().split(".").at(-1); @@ -53,11 +54,26 @@ function HtmlEvidenceViewer({ url }: { url: string }) { return ; } +export function VisualEvidenceModal({ open, onClose, title, runId, iteration, artifacts }: { open: boolean; onClose: () => void; title: string; runId: string; iteration: number; artifacts: EvidenceArtifact[] }) { + const imageArtifacts = artifacts.filter((artifact) => artifact.screenshot); + const htmlArtifacts = artifacts.filter((artifact) => artifact.html); + const tabs = [ + ...imageArtifacts.map((artifact, index) => ({ id: `image-${index}`, label: artifact.screenshot!.split("/").at(-1) ?? `#${index + 1}`, artifact })), + ...(htmlArtifacts.length ? [{ id: "html", label: "HTML" }] : []), + ]; + const [activeTab, setActiveTab] = useState(tabs[0]?.id ?? ""); + const selectedImage = imageArtifacts[Number(activeTab.replace("image-", ""))]; + return + {tabs.length > 1 &&
{tabs.map((tab) => )}
} + {activeTab === "html" ? htmlArtifacts.map((artifact, index) =>
{artifact.html!.split("/").at(-1) ?? artifact.name ?? `#${index + 1}`}
) : selectedImage?.screenshot &&
{selectedImage.screenshot.split("/").at(-1) ?? selectedImage.name}
{selectedImage.name
} +
; +} + export function EvidenceViewer({ runId, iteration, artifacts, className, imageLabel, htmlLabel, imageTitle, htmlTitle }: { runId: string; iteration: number; artifacts: EvidenceArtifact[]; className?: string; imageLabel: string; htmlLabel: string; imageTitle: string; htmlTitle: string }) { const [evidenceModal, setEvidenceModal] = useState<{ kind: "image" | "html"; artifacts: EvidenceArtifact[] } | null>(null); const imageArtifacts = artifacts.filter((artifact) => artifact.screenshot); const htmlArtifacts = artifacts.filter((artifact) => artifact.html); const modalArtifacts = evidenceModal?.artifacts.filter((artifact) => evidenceModal.kind === "image" ? Boolean(artifact.screenshot) : Boolean(artifact.html)) ?? []; if (!imageArtifacts.length && !htmlArtifacts.length) return null; - return <>{imageArtifacts.length > 0 && }{htmlArtifacts.length > 0 && }{evidenceModal && createPortal( setEvidenceModal(null)} className="modal--evidence">{modalArtifacts.map((artifact, index) =>
{artifact.name ?? artifact.id ?? `#${index + 1}`}
{evidenceModal.kind === "image" ? {artifact.name : }
)}
, document.body)}; + return <>{imageArtifacts.length > 0 && }{htmlArtifacts.length > 0 && }{evidenceModal && createPortal( setEvidenceModal(null)} title={evidenceModal.kind === "image" ? imageTitle : htmlTitle} runId={runId} iteration={iteration} artifacts={modalArtifacts} />, document.body)}; } diff --git a/frontend/src/features/improvements/page.tsx b/frontend/src/features/improvements/page.tsx index d2195e3f..c69b074f 100644 --- a/frontend/src/features/improvements/page.tsx +++ b/frontend/src/features/improvements/page.tsx @@ -46,6 +46,7 @@ import { DataTable, type Column } from "../../components/ui/data-table"; import { CircleStop, RotateCcw } from "lucide-react"; import { EvaluationsPage } from "../evaluations/page"; import { IssueManagementSection } from "../issues/page"; +import { VisualEvidenceModal, visualEvidenceArtifacts } from "../../components/ui/evidence-viewer"; type ImprovementCopy = { improvement: string; @@ -223,17 +224,18 @@ type PersonaJourneyEvent = { }; function PersonaJourneyTimeline({ - buildId, runs, locale, t, hours, onSelect, + buildId, runs, locale, t, hours, }: { buildId: string; runs: Run[]; locale: Locale; t: (typeof copy)["en"]; hours: number; - onSelect: (run: Run) => void; }) { const [rangeStart] = useState(() => hours ? Date.now() - hours * 60 * 60 * 1000 : 0); const drag = useRef<{ pointerId: number; startX: number; startScroll: number; moved: boolean } | null>(null); + const [isDragging, setIsDragging] = useState(false); + const [selectedEvent, setSelectedEvent] = useState(null); const lanes = useMemo(() => { const groups = new Map(); for (const run of runs) { @@ -297,32 +299,41 @@ function PersonaJourneyTimeline({ }, [buildId, rangeStart, runs]); const startDrag = (event: ReactPointerEvent) => { if (event.button !== 0) return; - event.currentTarget.setPointerCapture(event.pointerId); drag.current = { pointerId: event.pointerId, startX: event.clientX, startScroll: event.currentTarget.scrollLeft, moved: false }; }; const moveDrag = (event: ReactPointerEvent) => { const active = drag.current; if (!active || active.pointerId !== event.pointerId) return; const distance = event.clientX - active.startX; - if (Math.abs(distance) > 4) active.moved = true; + if (!active.moved && Math.abs(distance) <= 6) return; + if (!active.moved) { + active.moved = true; + event.currentTarget.setPointerCapture(event.pointerId); + setIsDragging(true); + } event.currentTarget.scrollLeft = active.startScroll - distance; }; const endDrag = (event: ReactPointerEvent) => { - if (drag.current?.pointerId === event.pointerId) event.currentTarget.releasePointerCapture(event.pointerId); + if (drag.current?.pointerId !== event.pointerId) return; + if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); + if (!drag.current.moved) drag.current = null; + setIsDragging(false); }; + const selectedArtifacts = selectedEvent ? visualEvidenceArtifacts((selectedEvent.run.step_results ?? []).filter((step) => step.loop_index === selectedEvent.iteration).flatMap((step) => step.data_files ?? [])) : []; return

{t.personaJourneysHint}

{lanes.length ?
{lanes.map((lane) =>
{lane.persona}{lane.events.length}
-
{ +
{ if (!drag.current?.moved) return; event.preventDefault(); event.stopPropagation(); drag.current = null; + setIsDragging(false); }}> - {lane.events.map((event) =>
)}
:

{t.noPersonaJourneys}

} + {selectedEvent && setSelectedEvent(null)} title={`${selectedEvent.persona} · ${t.iteration} #${selectedEvent.iteration}`} runId={selectedEvent.run.id} iteration={selectedEvent.iteration} artifacts={selectedArtifacts} />}
; } function Card({ @@ -771,7 +783,7 @@ function ImprovementBuildContent({ onRetryRequest={setRetryingRun} onSelect={setSelectedRun} /> - + {selectedRun && summary::before { content:'▾'; }.proposal-tree__build { padding:12px; border:1px solid var(--line)!important; border-radius:8px; background:var(--bg); }.proposal-tree__build > summary > strong { font-size:13px; }.proposal-tree summary small { color:var(--muted); font:10px 'DM Mono',monospace; }.proposal-tree__run { margin:10px 0 0 11px; padding:10px 0 0 13px; }.proposal-tree__run > summary { justify-content:flex-start; }.proposal-tree__run > summary span,.proposal-tree__iteration > summary span { display:grid; gap:3px; }.proposal-tree__run > summary strong { font-size:11px; }.proposal-tree__iteration { margin:9px 0 0 11px; padding:9px 0 0 13px; }.proposal-tree__iteration > summary { padding:8px 10px; border:1px solid var(--line); border-radius:7px; background:var(--surface-raised); }.proposal-tree__count { display:inline!important; margin-left:2px; color:var(--muted); font:400 10px 'DM Mono',monospace; }.proposal-tree__iteration > div { display:grid; gap:6px; margin:7px 0 0 12px; }.proposal-tree__item { display:flex; align-items:center; justify-content:space-between; gap:12px; width:100%; padding:10px; border:1px solid var(--line); border-radius:7px; color:var(--text); background:var(--surface); text-align:left; }.proposal-tree__item:hover { border-color:var(--accent); background:var(--surface-raised); }.proposal-tree__item > span { display:grid; gap:3px; min-width:0; }.proposal-tree__item strong { overflow:hidden; font-size:12px; text-overflow:ellipsis; white-space:nowrap; }.proposal-tree__item small { color:var(--muted); font-size:10px; } .proposal-tree__iteration-meta,.proposal-tree__item-meta { display:flex!important; align-items:center; gap:8px; flex:none; }.proposal-tree__iteration > summary .proposal-tree__evidence { display:flex!important; flex-direction:row; gap:5px; align-items:center; }.proposal-tree__score { padding:3px 6px; border:1px solid color-mix(in srgb,var(--accent) 45%,var(--line)); border-radius:999px; color:var(--accent); background:color-mix(in srgb,var(--accent) 10%,var(--bg)); font:600 10px 'DM Mono',monospace; white-space:nowrap; }.proposal-tree__persona-group { display:grid; gap:7px; padding:10px; border:1px solid var(--line); border-radius:7px; background:var(--bg); }.proposal-tree__persona-group > header { display:flex; align-items:baseline; }.proposal-tree__persona-group > header strong { font:600 10px 'DM Mono',monospace; }.proposal-tree__persona-group > div { display:grid; gap:6px; } -.persona-journeys { display:grid; gap:12px; }.persona-journeys > h3,.persona-journeys > .hint { margin:0; }.persona-journeys__lanes { display:grid; gap:12px; }.persona-journeys__lane { display:grid; grid-template-columns:160px minmax(0,1fr); gap:12px; padding:12px 0; border-top:1px solid var(--line); }.persona-journeys__lane > header { display:grid; align-content:start; gap:5px; }.persona-journeys__lane > header strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font:600 11px 'DM Mono',monospace; }.persona-journeys__lane > header small { color:var(--muted); font:10px 'DM Mono',monospace; }.persona-journeys__events { display:flex; gap:10px; overflow-x:auto; padding-bottom:4px; cursor:grab; touch-action:pan-y; }.persona-journeys__events:active { cursor:grabbing; }.persona-journeys__event { display:grid; flex:0 0 min(300px,78vw); gap:6px; padding:10px; border:1px solid var(--line); border-top:2px solid var(--accent); border-radius:7px; color:var(--text); background:var(--bg); text-align:left; cursor:pointer; }.persona-journeys__event:hover { background:var(--surface-raised); border-color:var(--accent); }.persona-journeys__event time { font:600 10px 'DM Mono',monospace; }.persona-journeys__event > small { color:var(--muted); font:9px 'DM Mono',monospace; }.persona-journeys__event dl { display:grid; gap:7px; margin:0; }.persona-journeys__event dl > div { display:grid; gap:2px; }.persona-journeys__event dt { color:var(--muted); font-size:9px; }.persona-journeys__event dd { display:-webkit-box; margin:0; overflow:hidden; font-size:11px; line-height:1.45; -webkit-box-orient:vertical; -webkit-line-clamp:3; } @media(max-width:720px){.persona-journeys__lane { grid-template-columns:1fr; gap:8px; }.persona-journeys__lane > header { grid-template-columns:minmax(0,1fr) auto; align-items:center; }.persona-journeys__event { flex-basis:min(300px,88vw); }} +.persona-journeys { display:grid; gap:12px; }.persona-journeys > h3,.persona-journeys > .hint { margin:0; }.persona-journeys__lanes { display:grid; gap:12px; }.persona-journeys__lane { display:grid; grid-template-columns:160px minmax(0,1fr); gap:12px; padding:12px 0; border-top:1px solid var(--line); }.persona-journeys__lane > header { display:grid; align-content:start; gap:5px; }.persona-journeys__lane > header strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font:600 11px 'DM Mono',monospace; }.persona-journeys__lane > header small { color:var(--muted); font:10px 'DM Mono',monospace; }.persona-journeys__events { display:flex; gap:10px; overflow-x:auto; padding-bottom:4px; cursor:grab; touch-action:pan-y; }.persona-journeys__events.is-dragging { cursor:grabbing; }.persona-journeys__event { display:grid; flex:0 0 min(300px,78vw); gap:6px; padding:10px; border:1px solid var(--line); border-top:2px solid var(--accent); border-radius:7px; color:var(--text); background:var(--bg); text-align:left; cursor:pointer; }.persona-journeys__event:hover { background:var(--surface-raised); border-color:var(--accent); }.persona-journeys__event time { font:600 10px 'DM Mono',monospace; }.persona-journeys__event > small { color:var(--muted); font:9px 'DM Mono',monospace; }.persona-journeys__event dl { display:grid; gap:7px; margin:0; }.persona-journeys__event dl > div { display:grid; gap:2px; }.persona-journeys__event dt { color:var(--muted); font-size:9px; }.persona-journeys__event dd { display:-webkit-box; margin:0; overflow:hidden; font-size:11px; line-height:1.45; -webkit-box-orient:vertical; -webkit-line-clamp:3; } @media(max-width:720px){.persona-journeys__lane { grid-template-columns:1fr; gap:8px; }.persona-journeys__lane > header { grid-template-columns:minmax(0,1fr) auto; align-items:center; }.persona-journeys__event { flex-basis:min(300px,88vw); }} .cycle-interventions__head { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }.cycle-interventions__head .panel-head { margin:0; }.cycle-interventions__head label { display:flex!important; align-items:center; gap:8px; color:var(--muted); font-size:11px; white-space:nowrap; }.cycle-interventions__head select { width:auto; min-width:76px; }.cycle-health { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px; margin:16px 0; }.cycle-health article { display:grid; gap:5px; padding:12px; border:1px solid var(--line); border-radius:8px; background:var(--bg); }.cycle-health small { color:var(--muted); font-size:10px; }.cycle-health strong { font:600 14px 'DM Mono',monospace; }.cycle-ai-action { display:grid; gap:10px; margin:16px 0; }.cycle-ai-action .approve { justify-self:start; }.cycle-ai-action pre { max-height:340px; overflow:auto; margin:0; padding:12px; border:1px solid var(--line); border-radius:8px; background:var(--bg); white-space:pre-wrap; font:11px/1.6 'DM Mono',monospace; } @media(max-width:720px){.cycle-interventions__head { flex-direction:column; }.cycle-interventions__head select { min-width:76px; }.cycle-health { grid-template-columns:1fr; }} .cycle-ai-response { max-height:340px; overflow:auto; padding:14px; border:1px solid var(--line); border-radius:8px; background:var(--bg); font-size:12px; line-height:1.65; }.cycle-ai-response > :first-child { margin-top:0; }.cycle-ai-response > :last-child { margin-bottom:0; }.cycle-ai-response h1,.cycle-ai-response h2,.cycle-ai-response h3 { margin:14px 0 7px; font-size:13px; }.cycle-ai-response p,.cycle-ai-response ul,.cycle-ai-response ol { margin:8px 0; }.cycle-ai-response ul,.cycle-ai-response ol { padding-left:20px; }.cycle-ai-response code { font:10px 'DM Mono',monospace; }.cycle-ai-response pre { overflow:auto; padding:9px; border-radius:6px; background:var(--surface-raised); } .catalog-list { display:grid; gap:6px; max-height:360px; overflow-y:auto; overscroll-behavior:contain; }.catalog-row { width:100%; display:grid; min-height:28px; gap:4px; padding:0; border:0; border-top:1px solid var(--line); color:var(--text); background:transparent; text-align:left; }.catalog-row:first-child { border-top:0; }.catalog-row:hover { color:var(--accent); background:var(--surface-raised); }.catalog-row strong { font-size:12px; }.catalog-row span { color:var(--muted); font-size:11px; }.catalog-empty { margin:0; padding:14px 6px; color:var(--muted); font-size:12px; line-height:1.55; border-top:1px solid var(--line); }