shak3008 commited on
Commit
7cdaffb
·
1 Parent(s): 5cc6ebf

polling and sort by doc

Browse files
frontend/src/hooks/usePolling.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * usePolling — self-rescheduling setTimeout-based polling.
5
+ *
6
+ * Calls `callback` every `interval` ms while `active` is true.
7
+ * Stops automatically when `active` becomes false.
8
+ * Cleans up on unmount.
9
+ *
10
+ * Uses setTimeout (not setInterval) so each poll waits for the
11
+ * previous one to finish before scheduling the next — identical
12
+ * to the pattern used in Documents.jsx.
13
+ *
14
+ * @param {() => Promise<void>} callback Async function to call each tick.
15
+ * @param {number} interval Milliseconds between polls.
16
+ * @param {boolean} active Poll only while this is true.
17
+ */
18
+ export function usePolling(callback, interval, active) {
19
+ const timerRef = useRef(null);
20
+ const callbackRef = useRef(callback);
21
+
22
+ // Keep callbackRef current without restarting the effect.
23
+ useEffect(() => {
24
+ callbackRef.current = callback;
25
+ }, [callback]);
26
+
27
+ useEffect(() => {
28
+ if (!active) {
29
+ clearTimeout(timerRef.current);
30
+ timerRef.current = null;
31
+ return;
32
+ }
33
+
34
+ timerRef.current = setTimeout(async () => {
35
+ await callbackRef.current();
36
+ }, interval);
37
+
38
+ return () => {
39
+ clearTimeout(timerRef.current);
40
+ timerRef.current = null;
41
+ };
42
+ // Re-run whenever active or the data that drives active changes.
43
+ // interval is stable so including it has no cost.
44
+ }, [active, interval]);
45
+ }
frontend/src/pages/Activity.jsx CHANGED
@@ -1,7 +1,9 @@
1
  import { useCallback, useEffect, useState } from "react";
2
  import { useWorkspace } from "../hooks/useWorkspace";
 
3
  import { listActivity } from "../api/activity";
4
- import { Card, CardHeader, CardBody, Badge, LoadingState, EmptyState, Glossary } from "../components/ui";
 
5
  import { GLOSSARY } from "../utils/labels";
6
  import "./Activity.css";
7
 
@@ -29,6 +31,9 @@ const TYPE_TONE = {
29
  commit_created: "success",
30
  };
31
 
 
 
 
32
  export default function Activity() {
33
  const { workspace, loading: wsLoading } = useWorkspace();
34
  const [events, setEvents] = useState([]);
@@ -36,7 +41,11 @@ export default function Activity() {
36
  const [loadingMore, setLoadingMore] = useState(false);
37
  const [nextCursor, setNextCursor] = useState(null);
38
  const [hasMore, setHasMore] = useState(false);
 
 
 
39
 
 
40
  const load = useCallback(async () => {
41
  if (!workspace) return;
42
  setLoading(true);
@@ -53,6 +62,25 @@ export default function Activity() {
53
  }
54
  }, [workspace]);
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  const loadOlder = useCallback(async () => {
57
  if (!workspace || !nextCursor || loadingMore) return;
58
  setLoadingMore(true);
@@ -69,11 +97,37 @@ export default function Activity() {
69
  }
70
  }, [workspace, nextCursor, loadingMore]);
71
 
 
72
  useEffect(() => { load(); }, [load]);
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  if (wsLoading) return <LoadingState label="Loading workspace…" />;
75
  if (!workspace) return <EmptyState title="No workspace" description="Create a workspace to get started." />;
76
 
 
 
 
 
 
77
  return (
78
  <div className="dw-page">
79
  <header className="dw-page__header">
@@ -83,17 +137,35 @@ export default function Activity() {
83
 
84
  <Glossary terms={GLOSSARY.activity} />
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  {loading ? (
87
  <LoadingState label="Loading activity…" />
88
- ) : events.length === 0 ? (
89
  <EmptyState
90
- title="No activity yet"
91
- description="Upload a document or process a workflow to see activity here."
 
 
 
 
92
  />
93
  ) : (
94
  <>
95
  <div className="dw-activity__list">
96
- {events.map((event) => (
97
  <div key={event.id} className="dw-activity__item">
98
  <Badge tone={TYPE_TONE[event.type] || "default"}>
99
  {TYPE_LABELS[event.type] || event.type}
@@ -108,7 +180,8 @@ export default function Activity() {
108
  ))}
109
  </div>
110
 
111
- {hasMore && (
 
112
  <div className="dw-activity__load-more">
113
  <button
114
  className="dw-activity__load-more-btn"
 
1
  import { useCallback, useEffect, useState } from "react";
2
  import { useWorkspace } from "../hooks/useWorkspace";
3
+ import { usePolling } from "../hooks/usePolling";
4
  import { listActivity } from "../api/activity";
5
+ import { listDocuments } from "../api/documents";
6
+ import { Badge, LoadingState, EmptyState, Glossary } from "../components/ui";
7
  import { GLOSSARY } from "../utils/labels";
8
  import "./Activity.css";
9
 
 
31
  commit_created: "success",
32
  };
33
 
34
+ // How often to check for new events while the page is open.
35
+ const REFRESH_INTERVAL = 15000;
36
+
37
  export default function Activity() {
38
  const { workspace, loading: wsLoading } = useWorkspace();
39
  const [events, setEvents] = useState([]);
 
41
  const [loadingMore, setLoadingMore] = useState(false);
42
  const [nextCursor, setNextCursor] = useState(null);
43
  const [hasMore, setHasMore] = useState(false);
44
+ // Document filter: "" = All
45
+ const [docFilter, setDocFilter] = useState("");
46
+ const [documents, setDocuments] = useState([]);
47
 
48
+ // Initial full load — replaces whatever is in state.
49
  const load = useCallback(async () => {
50
  if (!workspace) return;
51
  setLoading(true);
 
62
  }
63
  }, [workspace]);
64
 
65
+ // Background refresh — prepends only events the list doesn't have yet.
66
+ const refresh = useCallback(async () => {
67
+ if (!workspace) return;
68
+ try {
69
+ const response = await listActivity(workspace.id, 50);
70
+ const fresh = response?.events ?? (Array.isArray(response) ? response : []);
71
+ if (fresh.length === 0) return;
72
+ setEvents((prev) => {
73
+ const existingIds = new Set(prev.map((e) => e.id));
74
+ const newOnes = fresh.filter((e) => !existingIds.has(e.id));
75
+ if (newOnes.length === 0) return prev;
76
+ return [...newOnes, ...prev];
77
+ });
78
+ } catch {
79
+ // silently handle
80
+ }
81
+ }, [workspace]);
82
+
83
+ // Load older events (manual pagination, appended to the bottom).
84
  const loadOlder = useCallback(async () => {
85
  if (!workspace || !nextCursor || loadingMore) return;
86
  setLoadingMore(true);
 
97
  }
98
  }, [workspace, nextCursor, loadingMore]);
99
 
100
+ // Initial load.
101
  useEffect(() => { load(); }, [load]);
102
 
103
+ // Load document list once for the dropdown.
104
+ useEffect(() => {
105
+ if (!workspace) return;
106
+ listDocuments(workspace.id)
107
+ .then((result) => { if (Array.isArray(result)) setDocuments(result); })
108
+ .catch(() => {});
109
+ }, [workspace]);
110
+
111
+ // Poll every 15 s to prepend new events while the page is open.
112
+ usePolling(refresh, REFRESH_INTERVAL, !!workspace);
113
+
114
+ // Apply document filter client-side.
115
+ // Events carry metadata.filename and/or metadata.document_id.
116
+ const visibleEvents = docFilter
117
+ ? events.filter((e) => {
118
+ const meta = e.metadata || {};
119
+ return meta.filename === docFilter;
120
+ })
121
+ : events;
122
+
123
  if (wsLoading) return <LoadingState label="Loading workspace…" />;
124
  if (!workspace) return <EmptyState title="No workspace" description="Create a workspace to get started." />;
125
 
126
+ // Build sorted unique filenames for the dropdown.
127
+ const docNames = Array.from(
128
+ new Set(documents.map((d) => d.filename).filter(Boolean))
129
+ ).sort();
130
+
131
  return (
132
  <div className="dw-page">
133
  <header className="dw-page__header">
 
137
 
138
  <Glossary terms={GLOSSARY.activity} />
139
 
140
+ {/* Document filter dropdown */}
141
+ <div className="dw-knowledge__filters">
142
+ <select
143
+ className="dw-knowledge__filter-select"
144
+ value={docFilter}
145
+ onChange={(e) => setDocFilter(e.target.value)}
146
+ >
147
+ <option value="">All documents</option>
148
+ {docNames.map((name) => (
149
+ <option key={name} value={name}>{name}</option>
150
+ ))}
151
+ </select>
152
+ </div>
153
+
154
  {loading ? (
155
  <LoadingState label="Loading activity…" />
156
+ ) : visibleEvents.length === 0 ? (
157
  <EmptyState
158
+ title={docFilter ? `No activity for "${docFilter}"` : "No activity yet"}
159
+ description={
160
+ docFilter
161
+ ? "Try selecting a different document or All documents."
162
+ : "Upload a document or process a workflow to see activity here."
163
+ }
164
  />
165
  ) : (
166
  <>
167
  <div className="dw-activity__list">
168
+ {visibleEvents.map((event) => (
169
  <div key={event.id} className="dw-activity__item">
170
  <Badge tone={TYPE_TONE[event.type] || "default"}>
171
  {TYPE_LABELS[event.type] || event.type}
 
180
  ))}
181
  </div>
182
 
183
+ {/* Only show "load older" when showing all events, not filtered */}
184
+ {!docFilter && hasMore && (
185
  <div className="dw-activity__load-more">
186
  <button
187
  className="dw-activity__load-more-btn"
frontend/src/pages/Dashboard.jsx CHANGED
@@ -1,6 +1,7 @@
1
  import { useCallback, useEffect, useState } from "react";
2
  import { useNavigate } from "react-router-dom";
3
  import { useWorkspace } from "../hooks/useWorkspace";
 
4
  import { getDashboardStats } from "../api/dashboard";
5
  import { listDocuments } from "../api/documents";
6
  import { listActivity } from "../api/activity";
@@ -28,6 +29,9 @@ const EVENT_ICONS = {
28
  commit_created: "💾",
29
  };
30
 
 
 
 
31
  export default function Dashboard() {
32
  const { workspace, loading: wsLoading } = useWorkspace();
33
  const navigate = useNavigate();
@@ -58,6 +62,10 @@ export default function Dashboard() {
58
 
59
  useEffect(() => { load(); }, [load]);
60
 
 
 
 
 
61
  if (wsLoading || loading) return <LoadingState label="Loading dashboard..." />;
62
  if (!workspace) return <EmptyState title="No workspace" description="Create a workspace to get started." />;
63
 
@@ -109,10 +117,12 @@ export default function Dashboard() {
109
  title="Recent Documents"
110
  action={
111
  <span style={{ display: "flex", alignItems: "center", gap: "var(--space-3)" }}>
112
- <span style={{ fontSize: "var(--text-xs)", color: "var(--color-success)", display: "flex", alignItems: "center", gap: "4px" }}>
113
- <span style={{ width: "6px", height: "6px", borderRadius: "50%", background: "var(--color-success)", display: "inline-block" }} />
114
- Live
115
- </span>
 
 
116
  <Button size="sm" variant="ghost" onClick={() => navigate("/documents")}>View all</Button>
117
  </span>
118
  }
 
1
  import { useCallback, useEffect, useState } from "react";
2
  import { useNavigate } from "react-router-dom";
3
  import { useWorkspace } from "../hooks/useWorkspace";
4
+ import { usePolling } from "../hooks/usePolling";
5
  import { getDashboardStats } from "../api/dashboard";
6
  import { listDocuments } from "../api/documents";
7
  import { listActivity } from "../api/activity";
 
29
  commit_created: "💾",
30
  };
31
 
32
+ const ACTIVE_STATUSES = new Set(["PENDING", "RUNNING"]);
33
+ const POLL_INTERVAL = 3000;
34
+
35
  export default function Dashboard() {
36
  const { workspace, loading: wsLoading } = useWorkspace();
37
  const navigate = useNavigate();
 
62
 
63
  useEffect(() => { load(); }, [load]);
64
 
65
+ // Poll every 3 s while any of the recent docs is actively processing.
66
+ const hasActive = recentDocs.some((d) => ACTIVE_STATUSES.has(d.workflow_status));
67
+ usePolling(load, POLL_INTERVAL, hasActive);
68
+
69
  if (wsLoading || loading) return <LoadingState label="Loading dashboard..." />;
70
  if (!workspace) return <EmptyState title="No workspace" description="Create a workspace to get started." />;
71
 
 
117
  title="Recent Documents"
118
  action={
119
  <span style={{ display: "flex", alignItems: "center", gap: "var(--space-3)" }}>
120
+ {hasActive && (
121
+ <span style={{ fontSize: "var(--text-xs)", color: "var(--color-success)", display: "flex", alignItems: "center", gap: "4px" }}>
122
+ <span style={{ width: "6px", height: "6px", borderRadius: "50%", background: "var(--color-success)", display: "inline-block" }} />
123
+ Live
124
+ </span>
125
+ )}
126
  <Button size="sm" variant="ghost" onClick={() => navigate("/documents")}>View all</Button>
127
  </span>
128
  }
frontend/src/pages/Knowledge.jsx CHANGED
@@ -1,7 +1,9 @@
1
  import { useCallback, useEffect, useState } from "react";
2
  import { useNavigate } from "react-router-dom";
3
  import { useWorkspace } from "../hooks/useWorkspace";
 
4
  import { listKnowledge } from "../api/knowledge";
 
5
  import { restoreProposal } from "../api/proposals";
6
  import {
7
  Card,
@@ -59,6 +61,9 @@ const STATUSES = [
59
  "REJECTED",
60
  ];
61
 
 
 
 
62
  function ConfidenceBar({ confidence }) {
63
  if (confidence == null) return null;
64
 
@@ -93,19 +98,14 @@ function EvidenceBlock({ evidence }) {
93
  return (
94
  <div className="dw-knowledge__evidence">
95
  {evidence.map((ev, i) => (
96
- <div
97
- key={i}
98
- className="dw-knowledge__evidence-item"
99
- >
100
  <p className="dw-knowledge__evidence-quote">
101
  "{ev.quote}"
102
  </p>
103
-
104
  <div className="dw-knowledge__evidence-meta">
105
  {ev.page_number && (
106
  <span>Page {ev.page_number}</span>
107
  )}
108
-
109
  {ev.section && (
110
  <span>{ev.section}</span>
111
  )}
@@ -124,6 +124,18 @@ export default function Knowledge() {
124
  const [loading, setLoading] = useState(true);
125
  const [typeFilter, setTypeFilter] = useState("");
126
  const [statusFilter, setStatusFilter] = useState("");
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  const load = useCallback(async () => {
129
  if (!workspace) return;
@@ -162,9 +174,18 @@ export default function Knowledge() {
162
  }
163
  }, [workspace, typeFilter, statusFilter]);
164
 
165
- useEffect(() => {
166
- load();
167
- }, [load]);
 
 
 
 
 
 
 
 
 
168
 
169
  if (wsLoading) {
170
  return <LoadingState label="Loading workspace…" />;
@@ -179,6 +200,11 @@ export default function Knowledge() {
179
  );
180
  }
181
 
 
 
 
 
 
182
  return (
183
  <div className="dw-page">
184
  <header className="dw-page__header">
@@ -192,13 +218,27 @@ export default function Knowledge() {
192
  <Glossary terms={GLOSSARY.knowledge} />
193
 
194
  <div className="dw-knowledge__filters">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  <select
196
  className="dw-knowledge__filter-select"
197
  value={typeFilter}
198
  onChange={(e) => setTypeFilter(e.target.value)}
199
  >
200
  <option value="">All types</option>
201
-
202
  {TYPES.filter(Boolean).map((type) => (
203
  <option key={type} value={type}>
204
  {KNOWLEDGE_TYPE_LABELS[type] || type}
@@ -206,13 +246,13 @@ export default function Knowledge() {
206
  ))}
207
  </select>
208
 
 
209
  <select
210
  className="dw-knowledge__filter-select"
211
  value={statusFilter}
212
  onChange={(e) => setStatusFilter(e.target.value)}
213
  >
214
  <option value="">All statuses</option>
215
-
216
  {STATUSES.filter(Boolean).map((status) => (
217
  <option key={status} value={status}>
218
  {KNOWLEDGE_STATUS_LABELS[status] || status}
@@ -223,15 +263,22 @@ export default function Knowledge() {
223
 
224
  {loading ? (
225
  <LoadingState label="Loading knowledge items…" />
226
- ) : items.length === 0 ? (
227
  <EmptyState
228
- title="No knowledge has been committed yet"
229
- description="Upload and process documents to begin building the knowledge register."
 
 
 
 
 
 
 
 
230
  />
231
  ) : (
232
  <div className="dw-knowledge__list">
233
- {items.map((item) => {
234
- // Derive display status from proposal state
235
  const displayStatus =
236
  item.proposal?.status === "ARCHIVED"
237
  ? "ARCHIVED"
@@ -239,92 +286,71 @@ export default function Knowledge() {
239
  const isArchived = item.proposal?.status === "ARCHIVED";
240
 
241
  return (
242
- <Card
243
- key={item.id}
244
- interactive
245
- onClick={() =>
246
- navigate(`/knowledge/${item.id}`)
247
- }
248
- >
249
- <CardBody>
250
- <div className="dw-knowledge__item-header">
251
- <Badge
252
- tone={
253
- TYPE_TONE[item.type] ||
254
- "default"
255
- }
256
- >
257
- {KNOWLEDGE_TYPE_LABELS[item.type] ||
258
- item.type}
259
- </Badge>
260
-
261
- <Badge
262
- tone={
263
- STATUS_TONE[displayStatus] ||
264
- "default"
265
- }
266
- >
267
- {KNOWLEDGE_STATUS_LABELS[displayStatus] ||
268
- displayStatus}
269
- </Badge>
270
-
271
- <ConfidenceBar
272
- confidence={item.confidence}
273
- />
274
- </div>
275
-
276
- <p className="dw-knowledge__item-title">
277
- {item.title}
278
- </p>
279
-
280
- <p className="dw-knowledge__item-value">
281
- {item.value}
282
- </p>
283
-
284
- {item.summary && (
285
- <p className="dw-knowledge__item-summary">
286
- {item.summary}
287
  </p>
288
- )}
289
 
290
- <EvidenceBlock
291
- evidence={item.evidence}
292
- />
293
 
294
- <div className="dw-knowledge__item-meta">
295
- {item.filename && (
296
- <span>{item.filename}</span>
 
297
  )}
298
 
299
- {item.created_at && (
300
- <span>
301
- {new Date(
302
- item.created_at
303
- ).toLocaleDateString()}
304
- </span>
305
- )}
306
- </div>
307
-
308
- {isArchived && item.proposal?.id && (
309
- <div className="dw-knowledge__item-actions">
310
- <Button
311
- size="sm"
312
- variant="warning"
313
- onClick={(e) => {
314
- e.stopPropagation();
315
- handleRestore(item.proposal.id, item.id);
316
- }}
317
- >
318
- Restore to review
319
- </Button>
320
  </div>
321
- )}
322
- </CardBody>
323
- </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  );
325
  })}
326
  </div>
327
  )}
328
  </div>
329
  );
330
- }
 
1
  import { useCallback, useEffect, useState } from "react";
2
  import { useNavigate } from "react-router-dom";
3
  import { useWorkspace } from "../hooks/useWorkspace";
4
+ import { usePolling } from "../hooks/usePolling";
5
  import { listKnowledge } from "../api/knowledge";
6
+ import { listDocuments } from "../api/documents";
7
  import { restoreProposal } from "../api/proposals";
8
  import {
9
  Card,
 
61
  "REJECTED",
62
  ];
63
 
64
+ // Poll every 3 s while any item is still PENDING (being processed).
65
+ const POLL_INTERVAL = 3000;
66
+
67
  function ConfidenceBar({ confidence }) {
68
  if (confidence == null) return null;
69
 
 
98
  return (
99
  <div className="dw-knowledge__evidence">
100
  {evidence.map((ev, i) => (
101
+ <div key={i} className="dw-knowledge__evidence-item">
 
 
 
102
  <p className="dw-knowledge__evidence-quote">
103
  "{ev.quote}"
104
  </p>
 
105
  <div className="dw-knowledge__evidence-meta">
106
  {ev.page_number && (
107
  <span>Page {ev.page_number}</span>
108
  )}
 
109
  {ev.section && (
110
  <span>{ev.section}</span>
111
  )}
 
124
  const [loading, setLoading] = useState(true);
125
  const [typeFilter, setTypeFilter] = useState("");
126
  const [statusFilter, setStatusFilter] = useState("");
127
+ // Document dropdown: "" means All
128
+ const [docFilter, setDocFilter] = useState("");
129
+ // List of uploaded documents for the dropdown
130
+ const [documents, setDocuments] = useState([]);
131
+
132
+ // Load the document list once (for the dropdown).
133
+ useEffect(() => {
134
+ if (!workspace) return;
135
+ listDocuments(workspace.id)
136
+ .then((result) => { if (Array.isArray(result)) setDocuments(result); })
137
+ .catch(() => {});
138
+ }, [workspace]);
139
 
140
  const load = useCallback(async () => {
141
  if (!workspace) return;
 
174
  }
175
  }, [workspace, typeFilter, statusFilter]);
176
 
177
+ useEffect(() => { load(); }, [load]);
178
+
179
+ // Poll every 3 s while any knowledge item is still PENDING —
180
+ // new items appear automatically as documents finish processing.
181
+ const hasActive = items.some((i) => i.status === "PENDING");
182
+ usePolling(load, POLL_INTERVAL, hasActive);
183
+
184
+ // Apply document filter client-side (items already carry filename +
185
+ // document_version_id from the API response).
186
+ const visibleItems = docFilter
187
+ ? items.filter((i) => i.filename === docFilter)
188
+ : items;
189
 
190
  if (wsLoading) {
191
  return <LoadingState label="Loading workspace…" />;
 
200
  );
201
  }
202
 
203
+ // Build sorted unique filenames for the dropdown.
204
+ const docNames = Array.from(
205
+ new Set(documents.map((d) => d.filename).filter(Boolean))
206
+ ).sort();
207
+
208
  return (
209
  <div className="dw-page">
210
  <header className="dw-page__header">
 
218
  <Glossary terms={GLOSSARY.knowledge} />
219
 
220
  <div className="dw-knowledge__filters">
221
+ {/* Document filter */}
222
+ <select
223
+ className="dw-knowledge__filter-select"
224
+ value={docFilter}
225
+ onChange={(e) => setDocFilter(e.target.value)}
226
+ >
227
+ <option value="">All documents</option>
228
+ {docNames.map((name) => (
229
+ <option key={name} value={name}>
230
+ {name}
231
+ </option>
232
+ ))}
233
+ </select>
234
+
235
+ {/* Type filter */}
236
  <select
237
  className="dw-knowledge__filter-select"
238
  value={typeFilter}
239
  onChange={(e) => setTypeFilter(e.target.value)}
240
  >
241
  <option value="">All types</option>
 
242
  {TYPES.filter(Boolean).map((type) => (
243
  <option key={type} value={type}>
244
  {KNOWLEDGE_TYPE_LABELS[type] || type}
 
246
  ))}
247
  </select>
248
 
249
+ {/* Status filter */}
250
  <select
251
  className="dw-knowledge__filter-select"
252
  value={statusFilter}
253
  onChange={(e) => setStatusFilter(e.target.value)}
254
  >
255
  <option value="">All statuses</option>
 
256
  {STATUSES.filter(Boolean).map((status) => (
257
  <option key={status} value={status}>
258
  {KNOWLEDGE_STATUS_LABELS[status] || status}
 
263
 
264
  {loading ? (
265
  <LoadingState label="Loading knowledge items…" />
266
+ ) : visibleItems.length === 0 ? (
267
  <EmptyState
268
+ title={
269
+ docFilter
270
+ ? `No knowledge items for "${docFilter}"`
271
+ : "No knowledge has been committed yet"
272
+ }
273
+ description={
274
+ docFilter
275
+ ? "Try selecting a different document or All documents."
276
+ : "Upload and process documents to begin building the knowledge register."
277
+ }
278
  />
279
  ) : (
280
  <div className="dw-knowledge__list">
281
+ {visibleItems.map((item) => {
 
282
  const displayStatus =
283
  item.proposal?.status === "ARCHIVED"
284
  ? "ARCHIVED"
 
286
  const isArchived = item.proposal?.status === "ARCHIVED";
287
 
288
  return (
289
+ <Card
290
+ key={item.id}
291
+ interactive
292
+ onClick={() => navigate(`/knowledge/${item.id}`)}
293
+ >
294
+ <CardBody>
295
+ <div className="dw-knowledge__item-header">
296
+ <Badge tone={TYPE_TONE[item.type] || "default"}>
297
+ {KNOWLEDGE_TYPE_LABELS[item.type] || item.type}
298
+ </Badge>
299
+
300
+ <Badge tone={STATUS_TONE[displayStatus] || "default"}>
301
+ {KNOWLEDGE_STATUS_LABELS[displayStatus] || displayStatus}
302
+ </Badge>
303
+
304
+ <ConfidenceBar confidence={item.confidence} />
305
+ </div>
306
+
307
+ <p className="dw-knowledge__item-title">
308
+ {item.title}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  </p>
 
310
 
311
+ <p className="dw-knowledge__item-value">
312
+ {item.value}
313
+ </p>
314
 
315
+ {item.summary && (
316
+ <p className="dw-knowledge__item-summary">
317
+ {item.summary}
318
+ </p>
319
  )}
320
 
321
+ <EvidenceBlock evidence={item.evidence} />
322
+
323
+ <div className="dw-knowledge__item-meta">
324
+ {item.filename && (
325
+ <span>{item.filename}</span>
326
+ )}
327
+ {item.created_at && (
328
+ <span>
329
+ {new Date(item.created_at).toLocaleDateString()}
330
+ </span>
331
+ )}
 
 
 
 
 
 
 
 
 
 
332
  </div>
333
+
334
+ {isArchived && item.proposal?.id && (
335
+ <div className="dw-knowledge__item-actions">
336
+ <Button
337
+ size="sm"
338
+ variant="warning"
339
+ onClick={(e) => {
340
+ e.stopPropagation();
341
+ handleRestore(item.proposal.id, item.id);
342
+ }}
343
+ >
344
+ Restore to review
345
+ </Button>
346
+ </div>
347
+ )}
348
+ </CardBody>
349
+ </Card>
350
  );
351
  })}
352
  </div>
353
  )}
354
  </div>
355
  );
356
+ }
frontend/src/pages/Workflows.jsx CHANGED
@@ -1,6 +1,7 @@
1
  import { useCallback, useEffect, useState } from "react";
2
  import { useNavigate, useSearchParams } from "react-router-dom";
3
  import { useWorkspace } from "../hooks/useWorkspace";
 
4
  import { listWorkflows } from "../api/workflows";
5
  import { retryDocument } from "../api/documents";
6
  import { Button, Badge, LoadingState, EmptyState, DataTable } from "../components/ui";
@@ -12,6 +13,9 @@ const STATUS_TONE = {
12
  COMPLETED: "success", FAILED: "danger", CANCELLED: "info",
13
  };
14
 
 
 
 
15
  function duration(started, completed) {
16
  if (!started || !completed) return "—";
17
  const ms = new Date(completed) - new Date(started);
@@ -39,6 +43,10 @@ export default function Workflows() {
39
 
40
  useEffect(() => { load(); }, [load]);
41
 
 
 
 
 
42
  if (wsLoading) return <LoadingState label="Loading..." />;
43
  if (!workspace) return <EmptyState title="No workspace" />;
44
 
 
1
  import { useCallback, useEffect, useState } from "react";
2
  import { useNavigate, useSearchParams } from "react-router-dom";
3
  import { useWorkspace } from "../hooks/useWorkspace";
4
+ import { usePolling } from "../hooks/usePolling";
5
  import { listWorkflows } from "../api/workflows";
6
  import { retryDocument } from "../api/documents";
7
  import { Button, Badge, LoadingState, EmptyState, DataTable } from "../components/ui";
 
13
  COMPLETED: "success", FAILED: "danger", CANCELLED: "info",
14
  };
15
 
16
+ const ACTIVE_STATUSES = new Set(["PENDING", "RUNNING"]);
17
+ const POLL_INTERVAL = 3000;
18
+
19
  function duration(started, completed) {
20
  if (!started || !completed) return "—";
21
  const ms = new Date(completed) - new Date(started);
 
43
 
44
  useEffect(() => { load(); }, [load]);
45
 
46
+ // Poll every 3 s while any workflow is PENDING or RUNNING.
47
+ const hasActive = workflows.some((w) => ACTIVE_STATUSES.has(w.status));
48
+ usePolling(load, POLL_INTERVAL, hasActive);
49
+
50
  if (wsLoading) return <LoadingState label="Loading..." />;
51
  if (!workspace) return <EmptyState title="No workspace" />;
52