|
| 1 | +import { useEffect, useRef, useState } from "react"; |
1 | 2 | import { useWorkflow } from "../context"; |
| 3 | +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; |
2 | 4 |
|
3 | 5 | interface Props { |
4 | 6 | workflowView: "canvas" | "outputs"; |
5 | 7 | onWorkflowViewChange: (v: "canvas" | "outputs") => void; |
6 | 8 | } |
7 | 9 |
|
8 | 10 | export function Toolbar({ workflowView, onWorkflowViewChange }: Props) { |
9 | | - const { activeWorkflow, saveWorkflow, executeWorkflow, executing } = useWorkflow(); |
| 11 | + const { activeWorkflow, saveWorkflow, executeWorkflow, executing, lastRunResults } = useWorkflow(); |
| 12 | + const [showResults, setShowResults] = useState(false); |
| 13 | + // Auto-open the popover when a run finishes with results. |
| 14 | + const wasExecutingRef = useRef(executing); |
| 15 | + useEffect(() => { |
| 16 | + if (wasExecutingRef.current && !executing && lastRunResults.length > 0) { |
| 17 | + setShowResults(true); |
| 18 | + } |
| 19 | + wasExecutingRef.current = executing; |
| 20 | + }, [executing, lastRunResults.length]); |
10 | 21 |
|
11 | 22 | const tabClass = (active: boolean) => |
12 | 23 | `px-3 py-1.5 text-xs font-semibold cursor-pointer transition-all ${ |
@@ -43,7 +54,47 @@ export function Toolbar({ workflowView, onWorkflowViewChange }: Props) { |
43 | 54 | > |
44 | 55 | {executing ? (<><span className="spinner !border-white/30 !border-t-white" /> Running...</>) : "▶ Execute"} |
45 | 56 | </button> |
| 57 | + {lastRunResults.length > 0 && ( |
| 58 | + <button |
| 59 | + className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold border border-border-dim cursor-pointer bg-white text-gray-700 hover:bg-surface-card" |
| 60 | + onClick={() => setShowResults(true)} |
| 61 | + title="Reopen the most recent run's results" |
| 62 | + > |
| 63 | + Last run ({lastRunResults.length}) |
| 64 | + </button> |
| 65 | + )} |
46 | 66 | </div> |
| 67 | + |
| 68 | + <Dialog open={showResults} onOpenChange={setShowResults}> |
| 69 | + <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto"> |
| 70 | + <DialogHeader> |
| 71 | + <DialogTitle>Last run · {lastRunResults.length} output{lastRunResults.length === 1 ? "" : "s"}</DialogTitle> |
| 72 | + </DialogHeader> |
| 73 | + <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-2"> |
| 74 | + {lastRunResults.map((r) => ( |
| 75 | + <div key={r.nodeId} className="border border-border-dim rounded-lg p-3 bg-surface-card"> |
| 76 | + <div className="flex items-center justify-between mb-2"> |
| 77 | + <span className="text-xs font-semibold text-gray-700">{r.label}</span> |
| 78 | + <span className="text-[10px] text-gray-400 uppercase tracking-wide">{r.type}</span> |
| 79 | + </div> |
| 80 | + {r.imageUrl && ( |
| 81 | + <img className="block w-full rounded border border-border-dim mb-2" src={r.imageUrl} alt={r.label} /> |
| 82 | + )} |
| 83 | + {r.imageUrls && r.imageUrls.length > 0 && ( |
| 84 | + <div className="grid grid-cols-2 gap-1 mb-2"> |
| 85 | + {r.imageUrls.map((u, i) => ( |
| 86 | + <img key={i} className="block w-full aspect-square object-cover rounded border border-border-dim" src={u} alt={`${r.label} ${i + 1}`} /> |
| 87 | + ))} |
| 88 | + </div> |
| 89 | + )} |
| 90 | + {r.text && ( |
| 91 | + <pre className="text-[11px] p-2 rounded bg-white border border-border-dim text-gray-700 max-h-[200px] overflow-y-auto whitespace-pre-wrap break-words">{r.text}</pre> |
| 92 | + )} |
| 93 | + </div> |
| 94 | + ))} |
| 95 | + </div> |
| 96 | + </DialogContent> |
| 97 | + </Dialog> |
47 | 98 | </div> |
48 | 99 | ); |
49 | 100 | } |
0 commit comments