tranth3truong's picture
Deploy public Scribe-only CarePath Space
cc678b9
Raw
History Blame Contribute Delete
6.06 kB
import { FormEvent, MouseEvent, useState } from "react";
import { API_BASE_URL } from "../api";
import type { Language } from "../copy";
type Feedback = {
reason: string;
comment?: string | null;
};
type ReviewRow = {
id: string;
source_text: string;
translation: string;
corrected_text: string | null;
risk_tier: string;
status: string;
feedback: Feedback[];
};
type ReviewResponse = {
items: ReviewRow[];
total: number;
};
type Filters = {
risk: string;
flagged: boolean;
low_confidence: boolean;
escalated: boolean;
};
const initialFilters: Filters = {
risk: "",
flagged: false,
low_confidence: false,
escalated: false,
};
function reviewUrl(filters: Filters, format = "json") {
const params = new URLSearchParams({ format });
if (filters.risk) {
params.set("risk", filters.risk);
}
if (filters.flagged) {
params.set("flagged", "1");
}
if (filters.low_confidence) {
params.set("low_confidence", "1");
}
if (filters.escalated) {
params.set("escalated", "1");
}
return `${API_BASE_URL}/api/admin/review?${params.toString()}`;
}
export function AdminReview({ language = "vi" }: { language?: Language }) {
const [token, setToken] = useState("");
const [filters, setFilters] = useState<Filters>(initialFilters);
const [rows, setRows] = useState<ReviewRow[]>([]);
const [total, setTotal] = useState(0);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function loadReview(event?: FormEvent<HTMLFormElement>) {
event?.preventDefault();
setLoading(true);
setError(null);
try {
const response = await fetch(reviewUrl(filters), {
headers: { "X-Admin-Token": token },
});
if (!response.ok) {
throw new Error(`review request failed: ${response.status}`);
}
const payload = (await response.json()) as ReviewResponse;
setRows(payload.items);
setTotal(payload.total);
} catch (caught) {
setError(caught instanceof Error ? caught.message : "review request failed");
} finally {
setLoading(false);
}
}
async function downloadCsv(event: MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
setError(null);
try {
const response = await fetch(reviewUrl(filters, "csv"), {
headers: { "X-Admin-Token": token },
});
if (!response.ok) {
throw new Error(`CSV request failed: ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "carepath-review.csv";
link.click();
URL.revokeObjectURL(url);
} catch (caught) {
setError(caught instanceof Error ? caught.message : "CSV request failed");
}
}
return (
<main className="workspace admin-review" lang={language}>
<header className="topbar">
<div>
<p className="eyebrow">Admin review</p>
<h1>Translation review</h1>
<a href="#/">{language === "vi" ? "Quay lại phiên dịch" : "Return to interpreter"}</a>
</div>
<a href="#" onClick={(event) => void downloadCsv(event)}>
Download CSV
</a>
</header>
<form className="admin-filters" onSubmit={(event) => void loadReview(event)}>
<label>
Admin token
<input
type="password"
value={token}
onChange={(event) => setToken(event.target.value)}
/>
</label>
<label>
Risk
<select
value={filters.risk}
onChange={(event) => setFilters((current) => ({ ...current, risk: event.target.value }))}
>
<option value="">Any</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</label>
<label>
<input
checked={filters.flagged}
type="checkbox"
onChange={(event) =>
setFilters((current) => ({ ...current, flagged: event.target.checked }))
}
/>
Flagged
</label>
<label>
<input
checked={filters.low_confidence}
type="checkbox"
onChange={(event) =>
setFilters((current) => ({ ...current, low_confidence: event.target.checked }))
}
/>
Low confidence
</label>
<label>
<input
checked={filters.escalated}
type="checkbox"
onChange={(event) =>
setFilters((current) => ({ ...current, escalated: event.target.checked }))
}
/>
Escalated
</label>
<button disabled={loading} type="submit">
{loading ? "Loading..." : "Load review"}
</button>
</form>
{error ? (
<p className="error" role="alert">
{error}
</p>
) : null}
<section className="admin-table" aria-label="Review results">
<p className="meta">{total} turns</p>
<table>
<thead>
<tr>
<th>Source</th>
<th>Translation</th>
<th>Correction</th>
<th>Risk tier</th>
<th>Status</th>
<th>Feedback</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>{row.source_text}</td>
<td>{row.translation}</td>
<td>{row.corrected_text || ""}</td>
<td>{row.risk_tier}</td>
<td>{row.status}</td>
<td>{row.feedback.map((item) => item.reason).join(", ")}</td>
</tr>
))}
</tbody>
</table>
</section>
</main>
);
}