feat(lanes): show proof gallery panel in the Workspace page (C)

This commit is contained in:
2026-08-04 16:25:09 +07:00
parent a545230746
commit fccfa5ad17
6 changed files with 178 additions and 0 deletions
+65
View File
@@ -58,6 +58,7 @@ import type {
Lane,
LaneFeature,
LaneCounts,
ProofFeature,
WSMessage,
} from "../lib/types";
import { eventBus } from "../lib/eventBus";
@@ -132,6 +133,7 @@ export function Workspace() {
const [viewedFeatureSlug, setViewedFeatureSlug] = useState<string | null>(null);
const [features, setFeatures] = useState<LaneFeature[]>([]);
const [viewedFeature, setViewedFeature] = useState<LaneFeature | null>(null);
const [proofFeatures, setProofFeatures] = useState<ProofFeature[]>([]);
// Run state
const [mode, setMode] = useState<RunMode>("conversation");
@@ -804,6 +806,28 @@ export function Workspace() {
};
}, [currentLane?.id, viewedFeatureSlug]);
// Proof gallery follows the selected lane.
useEffect(() => {
if (!currentLane) {
setProofFeatures([]);
return;
}
api.lanes.proof
.list(currentLane.id)
.then((data) => setProofFeatures(data.features))
.catch(() => setProofFeatures([]));
}, [currentLane?.id, viewedFeatureSlug]);
// Resolve the active feature slug: either the user-selected one, or the lane's active_feature_id
const activeFeatureSlug =
viewedFeatureSlug ??
(currentLane?.active_feature_id
? features.find((f) => f.id === currentLane?.active_feature_id)?.slug
: null);
const proofFeature = activeFeatureSlug
? proofFeatures.find((f) => f.slug === activeFeatureSlug)
: null;
// Only lock the page to the viewport when we're showing a live run session.
// The config-card screen needs normal page flow so the form is fully
// reachable on short windows. The run-session screen, however, owns the
@@ -1068,6 +1092,47 @@ export function Workspace() {
</p>
)}
</div>
{proofFeature &&
(Object.keys(proofFeature.groups).length > 0 || proofFeature.ticket_report) && (
<div data-testid="proof-gallery" className="mt-2">
{proofFeature.ticket_report && (
<a
href={`/api/lanes/${currentLane.id}/proof/${proofFeature.ticket_report}`}
target="_blank"
rel="noreferrer"
className="text-xs text-fg-muted"
>
{tLanes("proof.ticketReport")}
</a>
)}
{Object.entries(proofFeature.groups).map(([group, images]) => (
<div key={group} className="mt-1">
<span className="text-xs text-fg-muted">
{group} · {images.length}
</span>
<div className="flex flex-wrap gap-1">
{images.slice(0, 8).map((img) => (
<img
key={img}
loading="lazy"
className="h-16 w-16 rounded object-cover"
src={api.lanes.proof.imageUrl(
currentLane.id,
proofFeature.slug,
group,
img
)}
alt={img}
/>
))}
{images.length > 8 && (
<span className="text-xs text-fg-muted">+{images.length - 8}</span>
)}
</div>
</div>
))}
</div>
)}
<div className="flex min-h-0 flex-col gap-2 border-t border-border pt-3">
{consoleSection}
</div>