shak3008 commited on
Commit
2bf1685
·
1 Parent(s): 08c9e0f

feat: dedicated Workflow Detail page at /workflows/:workflowId

Browse files

- New WorkflowDetail page with pipeline, timeline, proposals, metrics
- Workflows list now navigates to /workflows/{id} (own page)
- Document Workspace remains at /documents/{docId}/{wfId} (doc-focused)
- Back button links to /workflows
- Both pages share same components (Pipeline, Timeline, ProposalReviewList)
- Separate concerns: Documents page = doc management, Workflows page = processing

frontend/src/App.jsx CHANGED
@@ -15,6 +15,7 @@ import Knowledge from "./pages/Knowledge";
15
  import KnowledgeDetail from "./pages/KnowledgeDetail";
16
  import Search from "./pages/Search";
17
  import Workflows from "./pages/Workflows";
 
18
  import Activity from "./pages/Activity";
19
  import Settings from "./pages/Settings";
20
 
@@ -37,6 +38,7 @@ export default function App() {
37
  <Route path="/knowledge/:itemId" element={<KnowledgeDetail />} />
38
  <Route path="/search" element={<Search />} />
39
  <Route path="/workflows" element={<Workflows />} />
 
40
  <Route path="/activity" element={<Activity />} />
41
  <Route path="/settings" element={<Settings />} />
42
  </Route>
 
15
  import KnowledgeDetail from "./pages/KnowledgeDetail";
16
  import Search from "./pages/Search";
17
  import Workflows from "./pages/Workflows";
18
+ import WorkflowDetail from "./pages/WorkflowDetail";
19
  import Activity from "./pages/Activity";
20
  import Settings from "./pages/Settings";
21
 
 
38
  <Route path="/knowledge/:itemId" element={<KnowledgeDetail />} />
39
  <Route path="/search" element={<Search />} />
40
  <Route path="/workflows" element={<Workflows />} />
41
+ <Route path="/workflows/:workflowId" element={<WorkflowDetail />} />
42
  <Route path="/activity" element={<Activity />} />
43
  <Route path="/settings" element={<Settings />} />
44
  </Route>
frontend/src/pages/WorkflowDetail.css ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .dw-wfdetail__back {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--space-2);
5
+ background: none;
6
+ border: none;
7
+ color: var(--color-accent-text);
8
+ font-size: var(--text-sm);
9
+ cursor: pointer;
10
+ padding: 0;
11
+ margin-bottom: var(--space-3);
12
+ }
13
+
14
+ .dw-wfdetail__back:hover {
15
+ text-decoration: underline;
16
+ }
17
+
18
+ .dw-wfdetail__status-message {
19
+ font-size: var(--text-sm);
20
+ color: var(--color-text-secondary);
21
+ margin: 0 0 var(--space-3);
22
+ }
23
+
24
+ .dw-wfdetail__meta {
25
+ display: grid;
26
+ grid-template-columns: auto 1fr;
27
+ gap: var(--space-2) var(--space-4);
28
+ font-size: var(--text-sm);
29
+ }
30
+
31
+ .dw-wfdetail__meta dt {
32
+ color: var(--color-text-muted);
33
+ }
34
+
35
+ .dw-wfdetail__why {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: var(--space-3);
39
+ }
40
+
41
+ .dw-wfdetail__why-item {
42
+ display: flex;
43
+ align-items: flex-start;
44
+ gap: var(--space-3);
45
+ padding: var(--space-3);
46
+ background: var(--color-surface-hover);
47
+ border-radius: var(--radius-md);
48
+ border-left: 3px solid var(--color-warning);
49
+ }
50
+
51
+ .dw-wfdetail__why-badge {
52
+ flex-shrink: 0;
53
+ font-size: 10px;
54
+ font-weight: 700;
55
+ padding: 2px 6px;
56
+ border-radius: var(--radius-sm);
57
+ text-transform: uppercase;
58
+ }
59
+
60
+ .dw-wfdetail__why-badge--fail {
61
+ background: var(--color-danger-soft);
62
+ color: var(--color-danger);
63
+ }
64
+
65
+ .dw-wfdetail__why-badge--warning {
66
+ background: var(--color-warning-soft);
67
+ color: var(--color-warning);
68
+ }
69
+
70
+ .dw-wfdetail__why-rule {
71
+ font-size: var(--text-sm);
72
+ font-weight: 600;
73
+ color: var(--color-text-primary);
74
+ margin: 0;
75
+ }
76
+
77
+ .dw-wfdetail__why-msg {
78
+ font-size: var(--text-sm);
79
+ color: var(--color-text-secondary);
80
+ margin: var(--space-1) 0 0;
81
+ }
frontend/src/pages/WorkflowDetail.jsx ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useParams, useNavigate } from "react-router-dom";
3
+ import { getWorkflow } from "../api/workflows";
4
+ import { getWorkflowMetrics } from "../api/metrics";
5
+ import { listPendingProposals } from "../api/proposals";
6
+ import { WorkflowStatusBadge } from "../components/workflow/WorkflowStatusBadge";
7
+ import { WorkflowPipeline } from "../components/workflow/WorkflowPipeline";
8
+ import { WorkflowTimeline } from "../components/workflow/WorkflowTimeline";
9
+ import { ProposalReviewList } from "../components/proposals/ProposalReviewList";
10
+ import { Card, CardHeader, CardBody, LoadingState, EmptyState, Glossary } from "../components/ui";
11
+ import { GLOSSARY } from "../utils/labels";
12
+ import "./WorkflowDetail.css";
13
+
14
+ const ACTIVE_STATUSES = new Set(["PENDING", "RUNNING"]);
15
+ const POLL_INTERVAL_MS = 3000;
16
+
17
+ const STATUS_MESSAGES = {
18
+ PENDING: "This workflow is queued and will begin shortly.",
19
+ RUNNING: "The document is being processed. Knowledge extraction and reconciliation are in progress.",
20
+ WAITING_FOR_REVIEW: "Human review required. Please review the proposals below.",
21
+ COMPLETED: "This workflow has finished. All knowledge has been committed.",
22
+ FAILED: "This workflow run ended in a failed state.",
23
+ CANCELLED: "This workflow run was cancelled.",
24
+ };
25
+
26
+ export default function WorkflowDetail() {
27
+ const { workflowId } = useParams();
28
+ const navigate = useNavigate();
29
+ const [workflow, setWorkflow] = useState(null);
30
+ const [loading, setLoading] = useState(true);
31
+ const [error, setError] = useState(null);
32
+ const [metrics, setMetrics] = useState(null);
33
+ const [timelineProposals, setTimelineProposals] = useState([]);
34
+ const pollRef = useRef(null);
35
+
36
+ const fetchWorkflow = useCallback(async () => {
37
+ if (!workflowId) return;
38
+ try {
39
+ const result = await getWorkflow(workflowId);
40
+ if (result?.detail) {
41
+ setError(result.detail);
42
+ setWorkflow(null);
43
+ } else {
44
+ setWorkflow(result);
45
+ setError(null);
46
+ if (result.status && !ACTIVE_STATUSES.has(result.status)) {
47
+ const m = await getWorkflowMetrics(workflowId);
48
+ if (m && !m.detail) setMetrics(m);
49
+ }
50
+ if (result.workspace_id && result.document_version_id) {
51
+ const props = await listPendingProposals(result.workspace_id, result.document_version_id);
52
+ if (Array.isArray(props)) setTimelineProposals(props);
53
+ }
54
+ }
55
+ } catch {
56
+ setError("Couldn't load workflow.");
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ }, [workflowId]);
61
+
62
+ useEffect(() => { setLoading(true); fetchWorkflow(); }, [fetchWorkflow]);
63
+
64
+ useEffect(() => {
65
+ if (!workflow) return;
66
+ if (ACTIVE_STATUSES.has(workflow.status)) {
67
+ pollRef.current = setTimeout(fetchWorkflow, POLL_INTERVAL_MS);
68
+ }
69
+ return () => { if (pollRef.current) clearTimeout(pollRef.current); };
70
+ }, [workflow, fetchWorkflow]);
71
+
72
+ if (!workflowId) return <EmptyState title="No workflow selected" />;
73
+ if (loading) return <LoadingState label="Loading workflow…" />;
74
+ if (error) return <EmptyState title="Error" description={error} />;
75
+ if (!workflow) return <EmptyState title="Workflow not found" />;
76
+
77
+ return (
78
+ <div className="dw-page">
79
+ <button className="dw-wfdetail__back" onClick={() => navigate("/workflows")}>
80
+ ← Workflows
81
+ </button>
82
+ <header className="dw-page__header">
83
+ <h1>{workflow.filename || "Workflow"}</h1>
84
+ </header>
85
+
86
+ <Glossary terms={GLOSSARY.workflows} />
87
+
88
+ <Card>
89
+ <CardHeader
90
+ title="Workflow status"
91
+ action={<WorkflowStatusBadge status={workflow.status} />}
92
+ />
93
+ <CardBody>
94
+ <WorkflowPipeline
95
+ currentNode={workflow.current_node}
96
+ workflowStatus={workflow.status}
97
+ />
98
+ <p className="dw-wfdetail__status-message">
99
+ {STATUS_MESSAGES[workflow.status] || ""}
100
+ </p>
101
+ <dl className="dw-wfdetail__meta">
102
+ <dt>Started</dt>
103
+ <dd>{new Date(workflow.started_at).toLocaleString()}</dd>
104
+ <dt>Completed</dt>
105
+ <dd>{workflow.completed_at ? new Date(workflow.completed_at).toLocaleString() : "—"}</dd>
106
+ <dt>Document</dt>
107
+ <dd>{workflow.filename || "—"}</dd>
108
+ </dl>
109
+ </CardBody>
110
+ </Card>
111
+
112
+ {workflow.status === "WAITING_FOR_REVIEW" && workflow.validation_results && (
113
+ <Card>
114
+ <CardHeader title="Why is this waiting?" />
115
+ <CardBody>
116
+ <div className="dw-wfdetail__why">
117
+ {workflow.validation_results
118
+ .filter((r) => r.status === "FAIL" || r.status === "WARNING")
119
+ .map((r, i) => (
120
+ <div key={i} className="dw-wfdetail__why-item">
121
+ <span className={`dw-wfdetail__why-badge dw-wfdetail__why-badge--${r.status.toLowerCase()}`}>
122
+ {r.status}
123
+ </span>
124
+ <div>
125
+ <p className="dw-wfdetail__why-rule">{r.rule_name || "Validation Rule"}</p>
126
+ <p className="dw-wfdetail__why-msg">{r.message}</p>
127
+ </div>
128
+ </div>
129
+ ))}
130
+ </div>
131
+ </CardBody>
132
+ </Card>
133
+ )}
134
+
135
+ <Card>
136
+ <CardHeader title="Event timeline" />
137
+ <CardBody>
138
+ <WorkflowTimeline workflow={workflow} proposals={timelineProposals} />
139
+ </CardBody>
140
+ </Card>
141
+
142
+ {workflow.status === "WAITING_FOR_REVIEW" && (
143
+ <Card>
144
+ <CardHeader
145
+ title="Pending review"
146
+ subtitle="This workflow is paused until these proposals are approved or rejected."
147
+ />
148
+ <CardBody>
149
+ <ProposalReviewList
150
+ workspaceId={workflow.workspace_id}
151
+ documentVersionId={workflow.document_version_id}
152
+ onDecision={fetchWorkflow}
153
+ />
154
+ </CardBody>
155
+ </Card>
156
+ )}
157
+
158
+ {metrics && metrics.stages && metrics.stages.length > 0 && (
159
+ <Card>
160
+ <CardHeader title="Run cost and timing" />
161
+ <CardBody>
162
+ <div className="dw-metrics">
163
+ <div className="dw-metrics__summary">
164
+ {metrics.total_elapsed_ms != null && (
165
+ <div className="dw-metrics__stat">
166
+ <span className="dw-metrics__stat-value">
167
+ {metrics.total_elapsed_ms > 1000
168
+ ? `${(metrics.total_elapsed_ms / 1000).toFixed(1)}s`
169
+ : `${Math.round(metrics.total_elapsed_ms)}ms`}
170
+ </span>
171
+ <span className="dw-metrics__stat-label">Total time</span>
172
+ </div>
173
+ )}
174
+ {metrics.total_tokens > 0 && (
175
+ <div className="dw-metrics__stat">
176
+ <span className="dw-metrics__stat-value">{metrics.total_tokens.toLocaleString()}</span>
177
+ <span className="dw-metrics__stat-label">Tokens used</span>
178
+ </div>
179
+ )}
180
+ {metrics.total_llm_calls > 0 && (
181
+ <div className="dw-metrics__stat">
182
+ <span className="dw-metrics__stat-value">{metrics.total_llm_calls}</span>
183
+ <span className="dw-metrics__stat-label">LLM calls</span>
184
+ </div>
185
+ )}
186
+ </div>
187
+ <div className="dw-metrics__stages">
188
+ <p className="dw-metrics__stages-title">Time per stage</p>
189
+ {metrics.stages.map((stage) => (
190
+ <div key={stage.name} className="dw-metrics__stage-row">
191
+ <span className="dw-metrics__stage-name">{stage.name}</span>
192
+ <div className="dw-metrics__stage-bar-track">
193
+ <div
194
+ className="dw-metrics__stage-bar-fill"
195
+ style={{ width: `${Math.min(100, (stage.elapsed_ms / (metrics.total_elapsed_ms || 1)) * 100)}%` }}
196
+ />
197
+ </div>
198
+ <span className="dw-metrics__stage-time">
199
+ {stage.elapsed_ms > 1000 ? `${(stage.elapsed_ms / 1000).toFixed(1)}s` : `${Math.round(stage.elapsed_ms)}ms`}
200
+ </span>
201
+ {stage.input_tokens > 0 && (
202
+ <span className="dw-metrics__stage-tokens">
203
+ {(stage.input_tokens + stage.output_tokens).toLocaleString()} tok
204
+ </span>
205
+ )}
206
+ </div>
207
+ ))}
208
+ </div>
209
+ </div>
210
+ </CardBody>
211
+ </Card>
212
+ )}
213
+ </div>
214
+ );
215
+ }
frontend/src/pages/Workflows.jsx CHANGED
@@ -62,9 +62,7 @@ export default function Workflows() {
62
  key={w.id}
63
  interactive
64
  onClick={() => {
65
- if (w.document_id) {
66
- navigate(`/documents/${w.document_id}/${w.id}`);
67
- }
68
  }}
69
  >
70
  <CardBody>
 
62
  key={w.id}
63
  interactive
64
  onClick={() => {
65
+ navigate(`/workflows/${w.workflow_id}`);
 
 
66
  }}
67
  >
68
  <CardBody>