KPrashanth commited on
Commit
20b0fd6
·
verified ·
1 Parent(s): a804415

Deploy Rachana Data Studio

Browse files
data_studio/api.py CHANGED
@@ -167,8 +167,12 @@ def overview(user: dict[str, Any] = Depends(current_user)) -> dict[str, Any]:
167
 
168
 
169
  @app.post("/api/review/claim/{queue_name}")
170
- def claim(queue_name: str, user: dict[str, Any] = Depends(require_permission("review"))) -> dict[str, Any]:
171
- return {"sample": claim_next_sample_with_enrichment(db, settings, queue_name, user["username"])}
 
 
 
 
172
 
173
 
174
  @app.post("/api/review/{sample_id}/release")
 
167
 
168
 
169
  @app.post("/api/review/claim/{queue_name}")
170
+ def claim(
171
+ queue_name: str,
172
+ dataset_key: str | None = None,
173
+ user: dict[str, Any] = Depends(require_permission("review")),
174
+ ) -> dict[str, Any]:
175
+ return {"sample": claim_next_sample_with_enrichment(db, settings, queue_name, user["username"], dataset_key)}
176
 
177
 
178
  @app.post("/api/review/{sample_id}/release")
data_studio/review.py CHANGED
@@ -56,7 +56,12 @@ def queue_overview(db: Database, settings: StudioSettings) -> dict[str, Any]:
56
  }
57
 
58
 
59
- def claim_next_sample(db: Database, queue_name: str, reviewer: str) -> dict[str, Any] | None:
 
 
 
 
 
60
  now = utc_now()
61
  expires_at = now + timedelta(minutes=LEASE_MINUTES)
62
  lease_filter = {
@@ -68,6 +73,8 @@ def claim_next_sample(db: Database, queue_name: str, reviewer: str) -> dict[str,
68
  {"lease.reviewer": reviewer},
69
  ],
70
  }
 
 
71
  candidate_ids = [
72
  row["_id"]
73
  for row in db["samples"].aggregate(
@@ -106,8 +113,9 @@ def claim_next_sample_with_enrichment(
106
  settings: StudioSettings,
107
  queue_name: str,
108
  reviewer: str,
 
109
  ) -> dict[str, Any] | None:
110
- return claim_next_sample(db, queue_name, reviewer)
111
 
112
 
113
  def next_sample(db: Database, queue_name: str) -> dict[str, Any] | None:
 
56
  }
57
 
58
 
59
+ def claim_next_sample(
60
+ db: Database,
61
+ queue_name: str,
62
+ reviewer: str,
63
+ dataset_key: str | None = None,
64
+ ) -> dict[str, Any] | None:
65
  now = utc_now()
66
  expires_at = now + timedelta(minutes=LEASE_MINUTES)
67
  lease_filter = {
 
73
  {"lease.reviewer": reviewer},
74
  ],
75
  }
76
+ if dataset_key:
77
+ lease_filter["source.dataset_key"] = dataset_key
78
  candidate_ids = [
79
  row["_id"]
80
  for row in db["samples"].aggregate(
 
113
  settings: StudioSettings,
114
  queue_name: str,
115
  reviewer: str,
116
+ dataset_key: str | None = None,
117
  ) -> dict[str, Any] | None:
118
+ return claim_next_sample(db, queue_name, reviewer, dataset_key=dataset_key)
119
 
120
 
121
  def next_sample(db: Database, queue_name: str) -> dict[str, Any] | None:
frontend/app/globals.css CHANGED
@@ -13,7 +13,11 @@
13
  .reviewStack{display:grid;gap:18px}
14
  .decisionCardBottom{width:100%}
15
  .connectedCard p{color:var(--muted);margin:0 0 12px}
 
 
 
16
  .connectedList{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
17
- .connectedChip{border:1px solid var(--line);border-radius:12px;padding:12px;background:#f7f8f4;display:grid;gap:4px}
 
18
  .connectedChip small{color:var(--muted)}
19
  @media(max-width:760px){.shellCollapsed{grid-template-columns:1fr}.sidebarToggle{width:100%}}
 
13
  .reviewStack{display:grid;gap:18px}
14
  .decisionCardBottom{width:100%}
15
  .connectedCard p{color:var(--muted);margin:0 0 12px}
16
+ .connectedHeader{display:flex;justify-content:space-between;gap:14px;align-items:end;margin-bottom:14px}
17
+ .connectedHeader label{display:grid;gap:6px;font-size:12px;font-weight:700;min-width:240px}
18
+ .connectedHeader select{border:1px solid var(--line);border-radius:10px;padding:10px 12px;background:#fff;color:var(--ink)}
19
  .connectedList{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
20
+ .connectedChip{border:1px solid var(--line);border-radius:12px;padding:12px;background:#f7f8f4;display:grid;gap:4px;text-align:left}
21
+ .connectedChip.active{border-color:var(--green);background:var(--green-soft)}
22
  .connectedChip small{color:var(--muted)}
23
  @media(max-width:760px){.shellCollapsed{grid-template-columns:1fr}.sidebarToggle{width:100%}}
frontend/app/page.tsx CHANGED
@@ -130,7 +130,7 @@ export default function Home() {
130
  const [message, setMessage] = useState("");
131
  const [authChecked, setAuthChecked] = useState(false);
132
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
133
- const canAccessAdmin = user ? user.permissions.some((p) => ["import", "export", "manage_sources", "manage_users"].includes(p)) : false;
134
 
135
  const refreshOverview = useCallback(async (authToken: string) => {
136
  const data = await api<{ queues: QueueCounts; sources: Source[]; user: User; token_progress: TokenProgress; collaboration: Collaboration }>("/api/overview", authToken);
@@ -160,11 +160,12 @@ export default function Home() {
160
  }).finally(() => setAuthChecked(true));
161
  }, [refreshOverview]);
162
 
163
- const claim = async (selectedQueue = queue) => {
164
  setBusy(true);
165
  setMessage("");
166
  try {
167
- const data = await api<{ sample: Sample | null }>(`/api/review/claim/${selectedQueue}`, token, { method: "POST" });
 
168
  setSample(data.sample);
169
  if (!data.sample) setMessage("This queue has no available pending samples.");
170
  } catch (error) {
@@ -253,7 +254,7 @@ export default function Home() {
253
  </aside>
254
  <main>
255
  {message && <div className="notice" role="status">{message}<button onClick={() => setMessage("")}>Dismiss</button></div>}
256
- {section === "review" && <ReviewWorkspace queue={queue} queues={queues} sources={sources} sample={sample} busy={busy} collaboration={collaboration} onQueue={changeQueue} onClaim={() => claim()} onSubmit={submitReview} />}
257
  {section === "overview" && <Overview queues={queues} sources={sources} tokenProgress={tokenProgress} collaboration={collaboration} />}
258
  {section === "admin" && <Admin token={token} sources={sources} permissions={user.permissions} onDone={() => refreshOverview(token)} />}
259
  </main>
@@ -286,8 +287,16 @@ type EditorState = { cleaned: Record<string, unknown>; tags: string; notes: stri
286
 
287
  type ReviewLine = { id: string; left: string; right: string };
288
 
289
- function ReviewWorkspace({ queue, queues, sources, sample, busy, collaboration, onQueue, onClaim, onSubmit }: { queue: string; queues: QueueCounts; sources: Source[]; sample: Sample | null; busy: boolean; collaboration: Collaboration | null; onQueue: (q: string) => void; onClaim: () => void; onSubmit: (action: string, editor: EditorState) => void }) {
290
- return <><header className="pageHeader"><div><span className="eyebrow">Review workspace</span><h1>Make one clear decision at a time.</h1><p>Each reviewer gets a random available sample. Token progress stays global across the whole team.</p></div><div className="headerStats">{collaboration && <div className="metricInline"><span>Total cleaned</span><strong>{collaboration.total_cleaned_samples.toLocaleString()}</strong></div>}{sample?.lease && <div className="lease">Reserved until {new Date(sample.lease.expires_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div>}</div></header><section className="tableCard connectedCard"><h2>Connected datasets</h2><p>All currently registered sources are listed here. If one is missing, reseed sources from Data operations.</p><div className="connectedList">{sources.map((source) => <div key={source.dataset_key} className="connectedChip"><strong>{source.name}</strong><small>{source.dataset_key}</small></div>)}</div></section><div className="queueTabs">{QUEUES.map((item) => <button key={item.id} className={queue === item.id ? "active" : ""} onClick={() => onQueue(item.id)}><strong>{item.label}</strong><span>{queues[item.id]?.pending || 0} pending</span></button>)}</div>{sample ? <ReviewEditor key={sample.sample_id} sample={sample} busy={busy} onSubmit={onSubmit} /> : <div className="emptyState"><div>✓</div><h2>Ready for the next item?</h2><p>{QUEUES.find((q) => q.id === queue)?.description}</p><button className="primary" onClick={onClaim} disabled={busy}>{busy ? "Finding an item..." : "Claim random sample"}</button></div>}</>;
 
 
 
 
 
 
 
 
291
  }
292
 
293
  function ReviewEditor({ sample, busy, onSubmit }: { sample: Sample; busy: boolean; onSubmit: (action: string, editor: EditorState) => void }) {
 
130
  const [message, setMessage] = useState("");
131
  const [authChecked, setAuthChecked] = useState(false);
132
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
133
+ const canAccessAdmin = user ? user.role === "admin" || user.permissions.some((p) => ["import", "export", "manage_sources", "manage_users"].includes(p)) : false;
134
 
135
  const refreshOverview = useCallback(async (authToken: string) => {
136
  const data = await api<{ queues: QueueCounts; sources: Source[]; user: User; token_progress: TokenProgress; collaboration: Collaboration }>("/api/overview", authToken);
 
160
  }).finally(() => setAuthChecked(true));
161
  }, [refreshOverview]);
162
 
163
+ const claim = async (selectedQueue = queue, selectedDatasetKey?: string) => {
164
  setBusy(true);
165
  setMessage("");
166
  try {
167
+ const datasetQuery = selectedDatasetKey ? `?dataset_key=${encodeURIComponent(selectedDatasetKey)}` : "";
168
+ const data = await api<{ sample: Sample | null }>(`/api/review/claim/${selectedQueue}${datasetQuery}`, token, { method: "POST" });
169
  setSample(data.sample);
170
  if (!data.sample) setMessage("This queue has no available pending samples.");
171
  } catch (error) {
 
254
  </aside>
255
  <main>
256
  {message && <div className="notice" role="status">{message}<button onClick={() => setMessage("")}>Dismiss</button></div>}
257
+ {section === "review" && <ReviewWorkspace queue={queue} queues={queues} sources={sources} sample={sample} busy={busy} collaboration={collaboration} onQueue={changeQueue} onClaim={(datasetKey) => claim(queue, datasetKey)} onSubmit={submitReview} />}
258
  {section === "overview" && <Overview queues={queues} sources={sources} tokenProgress={tokenProgress} collaboration={collaboration} />}
259
  {section === "admin" && <Admin token={token} sources={sources} permissions={user.permissions} onDone={() => refreshOverview(token)} />}
260
  </main>
 
287
 
288
  type ReviewLine = { id: string; left: string; right: string };
289
 
290
+ function ReviewWorkspace({ queue, queues, sources, sample, busy, collaboration, onQueue, onClaim, onSubmit }: { queue: string; queues: QueueCounts; sources: Source[]; sample: Sample | null; busy: boolean; collaboration: Collaboration | null; onQueue: (q: string) => void; onClaim: (datasetKey?: string) => void; onSubmit: (action: string, editor: EditorState) => void }) {
291
+ const [datasetKey, setDatasetKey] = useState(sources[0]?.dataset_key || "");
292
+ useEffect(() => {
293
+ if (sources.length && !sources.some((source) => source.dataset_key === datasetKey)) {
294
+ setDatasetKey(sources[0].dataset_key);
295
+ }
296
+ }, [datasetKey, sources]);
297
+ const selectedSource = sources.find((source) => source.dataset_key === datasetKey);
298
+ const selectedQueue = selectedSource?.queue_name || queue;
299
+ return <><header className="pageHeader"><div><span className="eyebrow">Review workspace</span><h1>Make one clear decision at a time.</h1><p>Each reviewer gets a random available sample. Token progress stays global across the whole team.</p></div><div className="headerStats">{collaboration && <div className="metricInline"><span>Total cleaned</span><strong>{collaboration.total_cleaned_samples.toLocaleString()}</strong></div>}{sample?.lease && <div className="lease">Reserved until {new Date(sample.lease.expires_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div>}</div></header><section className="tableCard connectedCard"><div className="connectedHeader"><div><h2>Connected datasets</h2><p>Pick a dataset, then claim from its queue. This keeps review focused without hiding other sources.</p></div><label>Active dataset<select value={datasetKey} onChange={(e) => { const next = e.target.value; setDatasetKey(next); const nextSource = sources.find((source) => source.dataset_key === next); if (nextSource) onQueue(nextSource.queue_name); }}>{sources.length ? null : <option value="">No connected datasets</option>}<option value="">Select a dataset</option>{sources.map((source) => <option key={source.dataset_key} value={source.dataset_key}>{source.name}</option>)}</select></label></div><div className="connectedList">{sources.map((source) => <button key={source.dataset_key} type="button" className={`connectedChip${source.dataset_key === datasetKey ? " active" : ""}`} onClick={() => { setDatasetKey(source.dataset_key); onQueue(source.queue_name); }}><strong>{source.name}</strong><small>{source.dataset_key}</small></button>)}</div></section><div className="queueTabs">{QUEUES.map((item) => <button key={item.id} className={queue === item.id ? "active" : ""} onClick={() => onQueue(item.id)}><strong>{item.label}</strong><span>{queues[item.id]?.pending || 0} pending</span></button>)}</div>{sample ? <ReviewEditor key={sample.sample_id} sample={sample} busy={busy} onSubmit={onSubmit} /> : <div className="emptyState"><div>✓</div><h2>Ready for the next item?</h2><p>{selectedSource ? `${selectedSource.name} is mapped to ${selectedQueue}.` : QUEUES.find((q) => q.id === queue)?.description}</p><button className="primary" onClick={() => onClaim(datasetKey || undefined)} disabled={busy}>{busy ? "Finding an item..." : "Claim random sample"}</button></div>}</>;
300
  }
301
 
302
  function ReviewEditor({ sample, busy, onSubmit }: { sample: Sample; busy: boolean; onSubmit: (action: string, editor: EditorState) => void }) {