Expand request review tooling and KB

This commit is contained in:
Andraxion 2026-06-27 01:12:35 -04:00
parent ab1dfbf029
commit cae21b61b7
16 changed files with 1258 additions and 241 deletions

View file

@ -53,6 +53,8 @@ type LauncherRequest = {
problemType?: string;
rawExcerpt?: string;
confidence?: number | null;
reviewRationale?: string;
reviewOptions?: string[];
notes?: string;
}>;
};
@ -60,6 +62,8 @@ type LauncherRequest = {
updatedAt: string;
};
type LauncherRequestAnalysisItem = NonNullable<NonNullable<LauncherRequest["analysis"]>["items"]>[number];
type LauncherRequestsPayload = {
requests?: LauncherRequest[];
};
@ -95,6 +99,10 @@ type ProcessPendingPayload = {
pid?: number;
};
type LauncherRequestMetaPayload = {
allowedTags?: string[];
};
type AdminAuthPayload = {
ok?: boolean;
accessGranted?: boolean;
@ -148,6 +156,108 @@ function isAdminAccessError(error: unknown): boolean {
|| text.includes("admin access is not configured");
}
function cloneLauncherRequest(requestEntry: LauncherRequest): LauncherRequest {
return JSON.parse(JSON.stringify(requestEntry)) as LauncherRequest;
}
function getPrimaryAnalysisItem(requestEntry: LauncherRequest): LauncherRequestAnalysisItem | null {
const items = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis?.items : [];
return items.length > 0 ? items[0] : null;
}
function formatConfidence(value: number | null | undefined): string {
if (!Number.isFinite(Number(value))) {
return "Unscored";
}
return `${Math.round(Number(value) * 100)}%`;
}
function escapePopupHtml(value: string): string {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function openReviewDetailsPopup(requestEntry: LauncherRequest): void {
const popup = window.open("", `worldshaper-review-${requestEntry.id}`, "popup=yes,width=840,height=760,resizable=yes,scrollbars=yes");
if (!popup) {
return;
}
const items = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis.items : [];
const renderedItems = items.map((item, index) => {
const reviewOptions = Array.isArray(item?.reviewOptions) ? item.reviewOptions : [];
return `
<section class="review-card">
<div class="review-kicker">Review Item ${index + 1}</div>
<h2>${escapePopupHtml(String(item?.title || `Request ${index + 1}`))}</h2>
<div class="review-meta">
<span>${escapePopupHtml(String(item?.primaryCategory || "Unsorted"))}</span>
<span>${escapePopupHtml(String(item?.statusRecommendation || "needs_review"))}</span>
<span>${escapePopupHtml(formatConfidence(item?.confidence))}</span>
</div>
<div class="review-block">
<h3>Review Rationale</h3>
<p>${escapePopupHtml(String(item?.reviewRationale || "No structured review rationale was returned."))}</p>
</div>
<div class="review-block">
<h3>Parsed Interpretation</h3>
<p>${escapePopupHtml(String(item?.parsedInterpretation || ""))}</p>
</div>
<div class="review-block">
<h3>Implementation Approach</h3>
<p>${escapePopupHtml(String(item?.implementationApproach || ""))}</p>
</div>
<div class="review-block">
<h3>Possible Options</h3>
${reviewOptions.length > 0
? `<ul>${reviewOptions.map((option) => `<li>${escapePopupHtml(String(option || ""))}</li>`).join("")}</ul>`
: "<p>No structured options were returned.</p>"}
</div>
<div class="review-block">
<h3>Notes</h3>
<p>${escapePopupHtml(String(item?.notes || "No extra notes."))}</p>
</div>
</section>
`;
}).join("");
popup.document.open();
popup.document.write(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Worldshaper Review Details</title>
<style>
body { margin: 0; font-family: Georgia, "Segoe UI", sans-serif; background: #08111f; color: #e8f2ff; }
main { max-width: 920px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; }
.hero { padding: 18px 20px; border: 1px solid #365782; border-radius: 14px; background: linear-gradient(180deg, rgba(17,32,63,.96), rgba(10,19,38,.98)); }
.hero h1 { margin: 0 0 8px; font-size: 28px; }
.hero p { margin: 0; color: #b8cfee; line-height: 1.6; white-space: pre-wrap; }
.review-card { padding: 18px 20px; border: 1px solid #365782; border-radius: 14px; background: rgba(17,32,63,.88); display: grid; gap: 12px; }
.review-kicker { color: #ffd166; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
.review-card h2, .review-block h3 { margin: 0; }
.review-meta { display: flex; flex-wrap: wrap; gap: 8px; }
.review-meta span { padding: 4px 8px; border: 1px solid #365782; border-radius: 999px; background: rgba(8,16,31,.75); font-size: 12px; }
.review-block { display: grid; gap: 6px; }
.review-block p, .review-block ul { margin: 0; color: #d7e7ff; line-height: 1.6; white-space: pre-wrap; }
.review-block ul { padding-left: 18px; }
</style>
</head>
<body>
<main>
<section class="hero">
<h1>${escapePopupHtml(requestEntry.title)}</h1>
<p>${escapePopupHtml(requestEntry.sourceText)}</p>
</section>
${renderedItems || '<section class="review-card"><h2>No Review Items</h2><p>This request does not have structured review details yet.</p></section>'}
</main>
</body>
</html>`);
popup.document.close();
}
function formatRequestTimestamp(value: string): string {
const parsed = Date.parse(String(value || ""));
if (!Number.isFinite(parsed)) {
@ -305,6 +415,7 @@ function WorldshaperLauncher() {
const [requestSubmitting, setRequestSubmitting] = useState(false);
const [requestMutatingId, setRequestMutatingId] = useState("");
const [requestFilter, setRequestFilter] = useState("all");
const [allowedRequestTags, setAllowedRequestTags] = useState<string[]>([]);
const [expandedRequestIds, setExpandedRequestIds] = useState<string[]>([]);
const [adminPanelOpen, setAdminPanelOpen] = useState(false);
const [adminAccessGranted, setAdminAccessGranted] = useState(false);
@ -312,6 +423,9 @@ function WorldshaperLauncher() {
const [adminPasswordDraft, setAdminPasswordDraft] = useState("");
const [adminAuthSubmitting, setAdminAuthSubmitting] = useState(false);
const [adminPasswordError, setAdminPasswordError] = useState("");
const [selectedAdminRequestId, setSelectedAdminRequestId] = useState("");
const [adminEditorDraft, setAdminEditorDraft] = useState<LauncherRequest | null>(null);
const [adminSaving, setAdminSaving] = useState(false);
const [recentSaveEvents, setRecentSaveEvents] = useState<RecentSaveEvent[]>([]);
const [logsLoading, setLogsLoading] = useState(false);
const [logsError, setLogsError] = useState("");
@ -357,6 +471,15 @@ function WorldshaperLauncher() {
}
}
async function loadRequestMeta(): Promise<void> {
try {
const payload = await fetchJsonOrThrow<LauncherRequestMetaPayload>("/api/launcher-request-meta");
setAllowedRequestTags(Array.isArray(payload.allowedTags) ? payload.allowedTags : []);
} catch {
setAllowedRequestTags([]);
}
}
async function loadRecentSaveEvents(): Promise<void> {
setLogsLoading(true);
try {
@ -397,6 +520,7 @@ function WorldshaperLauncher() {
useEffect(() => {
void loadRequests();
void loadRequestMeta();
}, []);
useEffect(() => {
@ -412,13 +536,15 @@ function WorldshaperLauncher() {
}
let cancelled = false;
const refreshBoard = async (): Promise<void> => {
try {
const payload = await fetchJsonOrThrow<LauncherRequestsPayload>("/api/launcher-requests");
if (!cancelled) {
setRequests(Array.isArray(payload.requests) ? payload.requests : []);
if (!adminPanelOpen) {
try {
const payload = await fetchJsonOrThrow<LauncherRequestsPayload>("/api/launcher-requests");
if (!cancelled) {
setRequests(Array.isArray(payload.requests) ? payload.requests : []);
}
} catch {
// Keep the current list visible during background refresh failures.
}
} catch {
// Keep the current list visible during background refresh failures.
}
if (!adminPanelOpen || !adminAccessGranted || !adminPassword) {
return;
@ -443,6 +569,26 @@ function WorldshaperLauncher() {
};
}, [activeBoardTab, adminPanelOpen, adminAccessGranted, adminPassword]);
useEffect(() => {
if (!adminPanelOpen || !adminAccessGranted) {
return;
}
if (requests.length === 0) {
setSelectedAdminRequestId("");
setAdminEditorDraft(null);
return;
}
const selectedRequest = requests.find((entry) => entry.id === selectedAdminRequestId);
if (selectedRequest) {
if (!adminEditorDraft || adminEditorDraft.id !== selectedRequest.id) {
setAdminEditorDraft(cloneLauncherRequest(selectedRequest));
}
return;
}
setSelectedAdminRequestId(requests[0].id);
setAdminEditorDraft(cloneLauncherRequest(requests[0]));
}, [adminPanelOpen, adminAccessGranted, requests, selectedAdminRequestId, adminEditorDraft]);
async function handleLaunch(): Promise<void> {
setError("");
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
@ -539,6 +685,152 @@ function WorldshaperLauncher() {
}
}
function handleSelectAdminRequest(requestId: string): void {
const nextRequest = requests.find((entry) => entry.id === requestId);
setSelectedAdminRequestId(requestId);
setAdminEditorDraft(nextRequest ? cloneLauncherRequest(nextRequest) : null);
setAdminNotice("");
setAdminPasswordError("");
}
function updateAdminDraft(updater: (current: LauncherRequest) => LauncherRequest): void {
setAdminEditorDraft((current) => (current ? updater(current) : current));
}
function updateAdminDraftItem(
itemIndex: number,
updater: (item: LauncherRequestAnalysisItem) => LauncherRequestAnalysisItem,
): void {
updateAdminDraft((current) => {
const next = cloneLauncherRequest(current);
if (!next.analysis) {
next.analysis = {
state: "needs_review",
items: [],
};
}
const items = Array.isArray(next.analysis.items) ? [...next.analysis.items] : [];
const existingItem = items[itemIndex] || {};
items[itemIndex] = updater({
...existingItem,
tags: Array.isArray(existingItem.tags) ? [...existingItem.tags] : [],
affectedSystems: Array.isArray(existingItem.affectedSystems) ? [...existingItem.affectedSystems] : [],
affectedFiles: Array.isArray(existingItem.affectedFiles) ? [...existingItem.affectedFiles] : [],
reviewOptions: Array.isArray(existingItem.reviewOptions) ? [...existingItem.reviewOptions] : [],
});
next.analysis.items = items;
next.analysis.itemCount = items.length;
next.analysis.updatedAt = new Date().toISOString();
return next;
});
}
function buildAdminSavePayload(requestEntry: LauncherRequest): RequestInit {
return {
method: "PATCH",
headers: buildAdminHeaders(adminPassword, {
"Content-Type": "application/json",
}),
body: JSON.stringify({
title: requestEntry.title,
status: requestEntry.status,
category: requestEntry.category,
tags: requestEntry.tags,
sourceText: requestEntry.sourceText,
summary: requestEntry.summary,
implementationNotes: requestEntry.implementationNotes,
analysis: requestEntry.analysis,
}),
};
}
async function handleSaveAdminRequest(): Promise<void> {
if (!adminEditorDraft) {
return;
}
setAdminSaving(true);
try {
const payload = await fetchJsonOrThrow<{ request?: LauncherRequest; requests?: LauncherRequest[] }>(
`/api/launcher-requests/${encodeURIComponent(adminEditorDraft.id)}`,
buildAdminSavePayload(adminEditorDraft),
);
const nextRequests = Array.isArray(payload.requests) ? payload.requests : requests;
setRequests(nextRequests);
const refreshed = nextRequests.find((entry) => entry.id === adminEditorDraft.id) || payload.request || adminEditorDraft;
setAdminEditorDraft(cloneLauncherRequest(refreshed));
setAdminNotice(`Saved admin changes for "${adminEditorDraft.title}".`);
if (adminPanelOpen) {
void loadRecentSaveEvents();
}
} catch (nextError: unknown) {
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
setLogsError(String(nextError || "Failed to save admin changes."));
} finally {
setAdminSaving(false);
}
}
async function handleApproveAdminRequest(): Promise<void> {
if (!adminEditorDraft) {
return;
}
const nextDraft = cloneLauncherRequest(adminEditorDraft);
if (!nextDraft.analysis) {
setLogsError("This request has no analysis to approve yet.");
return;
}
const items = Array.isArray(nextDraft.analysis.items) ? nextDraft.analysis.items : [];
nextDraft.analysis.items = items.map((item) => ({
...item,
statusRecommendation: "active",
}));
nextDraft.analysis.state = "processed";
nextDraft.analysis.updatedAt = new Date().toISOString();
setAdminEditorDraft(nextDraft);
setAdminSaving(true);
try {
await fetchJsonOrThrow<{ request?: LauncherRequest; requests?: LauncherRequest[] }>(
`/api/launcher-requests/${encodeURIComponent(nextDraft.id)}`,
buildAdminSavePayload(nextDraft),
);
const promotePayload = await fetchJsonOrThrow<LauncherRequestsPayload>(
`/api/launcher-requests/${encodeURIComponent(nextDraft.id)}/process-analysis`,
{
method: "POST",
headers: buildAdminHeaders(adminPassword, {
"Content-Type": "application/json",
}),
body: JSON.stringify({
action: "promote",
analysis: nextDraft.analysis,
}),
},
);
const nextRequests = Array.isArray(promotePayload.requests) ? promotePayload.requests : [];
setRequests(nextRequests);
const fallbackSelection = nextRequests[0] || null;
setSelectedAdminRequestId(fallbackSelection?.id || "");
setAdminEditorDraft(fallbackSelection ? cloneLauncherRequest(fallbackSelection) : null);
setAdminNotice(`Approved "${nextDraft.title}" and promoted its active request item${(nextDraft.analysis.items?.length || 0) === 1 ? "" : "s"}.`);
if (adminPanelOpen) {
void loadRecentSaveEvents();
}
} catch (nextError: unknown) {
if (isAdminAccessError(nextError)) {
setAdminAccessGranted(false);
setAdminPassword("");
setAdminPasswordError("Admin access expired. Enter the password again.");
}
setLogsError(String(nextError || "Failed to approve this request."));
} finally {
setAdminSaving(false);
}
}
function handleToggleExpandedRequest(requestId: string): void {
setExpandedRequestIds((current) => (
current.includes(requestId)
@ -622,12 +914,14 @@ function WorldshaperLauncher() {
const activeRequestCount = requests.filter((entry) => entry.status === "active").length;
const queuedPendingRequestCount = requests.filter(isQueuedPendingRequest).length;
const needsReviewRequestCount = requests.filter(isNeedsReviewRequest).length;
const requestTags = Array.from(new Set(
requests
.flatMap((entry) => Array.isArray(entry.tags) ? entry.tags : [])
.map((entry) => String(entry || "").trim())
.filter(Boolean),
)).sort((a, b) => a.localeCompare(b));
const requestTags = (allowedRequestTags.length > 0
? allowedRequestTags
: Array.from(new Set(
requests
.flatMap((entry) => Array.isArray(entry.tags) ? entry.tags : [])
.map((entry) => String(entry || "").trim())
.filter(Boolean),
))).sort((a, b) => a.localeCompare(b));
const filteredRequests = requests.filter((entry) => {
if (requestFilter === "status:pending") {
return entry.status === "pending";
@ -881,27 +1175,40 @@ function WorldshaperLauncher() {
<section className="launcher-request-admin-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Request Management</h4>
<div className="launcher-request-admin-card-hint">Delete actions live here now.</div>
<div className="launcher-request-admin-card-hint">Select a request to review or edit it.</div>
</div>
<div className="launcher-request-admin-request-list">
{requests.map((requestEntry) => {
const isMutating = requestMutatingId === requestEntry.id;
const analysisState = formatAnalysisStateLabel(requestEntry.analysis?.state);
const reviewItem = getPrimaryAnalysisItem(requestEntry);
const isSelected = requestEntry.id === selectedAdminRequestId;
return (
<article key={`admin-${requestEntry.id}`} className="launcher-request-admin-request-row">
<article
key={`admin-${requestEntry.id}`}
className={`launcher-request-admin-request-row ${isSelected ? "is-selected" : ""}`}
onClick={() => handleSelectAdminRequest(requestEntry.id)}
>
<div className="launcher-request-admin-request-copy">
<div className="launcher-request-admin-request-title">{requestEntry.title}</div>
<div className="launcher-request-admin-request-meta">
<span>{formatRequestStatusLabel(requestEntry.status)}</span>
<span>{requestEntry.category}</span>
<span>{analysisState}</span>
<span>{formatConfidence(reviewItem?.confidence ?? requestEntry.analysis?.confidence)}</span>
<span>{formatRequestTimestamp(requestEntry.updatedAt)}</span>
</div>
{reviewItem?.reviewRationale ? (
<div className="launcher-request-admin-request-rationale">{reviewItem.reviewRationale}</div>
) : null}
</div>
<button
type="button"
className="launcher-request-delete-btn"
onClick={() => void handleDeleteRequest(requestEntry)}
onClick={(event) => {
event.stopPropagation();
void handleDeleteRequest(requestEntry);
}}
disabled={isMutating}
aria-label={`Delete ${requestEntry.title}`}
>
@ -912,6 +1219,253 @@ function WorldshaperLauncher() {
})}
</div>
</section>
<section className="launcher-request-admin-card launcher-request-admin-editor-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Review Editor</h4>
<div className="launcher-request-admin-card-hint">Edit request fields, review details, and approval state.</div>
</div>
{!adminEditorDraft ? (
<div className="launcher-request-empty">Select a request from the list to review it.</div>
) : (
<div className="launcher-request-admin-editor">
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Title</span>
<input
type="text"
className="launcher-request-filter-select"
value={adminEditorDraft.title}
onChange={(event) => updateAdminDraft((current) => ({ ...current, title: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Category</span>
<input
type="text"
className="launcher-request-filter-select"
value={adminEditorDraft.category}
onChange={(event) => updateAdminDraft((current) => ({ ...current, category: event.target.value }))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Original Submission</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={adminEditorDraft.sourceText}
onChange={(event) => updateAdminDraft((current) => ({ ...current, sourceText: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Summary</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={adminEditorDraft.summary}
onChange={(event) => updateAdminDraft((current) => ({ ...current, summary: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Implementation Notes</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea-sm"
value={adminEditorDraft.implementationNotes}
onChange={(event) => updateAdminDraft((current) => ({ ...current, implementationNotes: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Request Tags</span>
<div className="launcher-request-admin-tag-grid">
{requestTags.map((tag) => {
const isActive = adminEditorDraft.tags.includes(tag);
return (
<button
key={`draft-tag-${tag}`}
type="button"
className={`launcher-request-tag launcher-request-admin-tag-toggle ${isActive ? "is-active" : ""}`}
onClick={() => updateAdminDraft((current) => ({
...current,
tags: isActive
? current.tags.filter((entry) => entry !== tag)
: [...current.tags, tag].sort((left, right) => left.localeCompare(right)),
}))}
>
{tag}
</button>
);
})}
</div>
</label>
{(adminEditorDraft.analysis?.items || []).map((item, itemIndex) => (
<section key={`analysis-item-${itemIndex}`} className="launcher-request-admin-analysis-item">
<div className="launcher-request-admin-analysis-head">
<div>
<div className="launcher-request-admin-kicker">Review Item {itemIndex + 1}</div>
<div className="launcher-request-admin-request-title">{item.title || `Request ${itemIndex + 1}`}</div>
</div>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => openReviewDetailsPopup(adminEditorDraft)}
>
Open Review Popup
</button>
</div>
<div className="launcher-request-admin-editor-grid">
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Item Title</span>
<input
type="text"
className="launcher-request-filter-select"
value={String(item.title || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, title: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Primary Category</span>
<input
type="text"
className="launcher-request-filter-select"
value={String(item.primaryCategory || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, primaryCategory: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Recommendation</span>
<select
className="launcher-request-filter-select"
value={String(item.statusRecommendation || "needs_review")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, statusRecommendation: event.target.value }))}
>
<option value="needs_review">Needs Review</option>
<option value="active">Active</option>
<option value="blocked">Blocked</option>
<option value="duplicate">Duplicate</option>
</select>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Problem Type</span>
<select
className="launcher-request-filter-select"
value={String(item.problemType || "unknown")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, problemType: event.target.value }))}
>
<option value="feature">Feature</option>
<option value="bug">Bug</option>
<option value="workflow">Workflow</option>
<option value="performance">Performance</option>
<option value="ux">UX</option>
<option value="content">Content</option>
<option value="unknown">Unknown</option>
</select>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Confidence</span>
<input
type="number"
min="0"
max="1"
step="0.01"
className="launcher-request-filter-select"
value={Number.isFinite(Number(item.confidence)) ? String(item.confidence) : ""}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({
...current,
confidence: event.target.value === "" ? null : Number(event.target.value),
}))}
/>
</label>
</div>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Standardized Tags</span>
<div className="launcher-request-admin-tag-grid">
{requestTags.map((tag) => {
const isActive = Array.isArray(item.tags) && item.tags.includes(tag);
return (
<button
key={`item-${itemIndex}-tag-${tag}`}
type="button"
className={`launcher-request-tag launcher-request-admin-tag-toggle ${isActive ? "is-active" : ""}`}
onClick={() => updateAdminDraftItem(itemIndex, (current) => {
const currentTags = Array.isArray(current.tags) ? current.tags : [];
return {
...current,
tags: isActive
? currentTags.filter((entry) => entry !== tag)
: [...currentTags, tag].sort((left, right) => left.localeCompare(right)),
};
})}
>
{tag}
</button>
);
})}
</div>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Review Rationale</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.reviewRationale || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, reviewRationale: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Possible Options</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={Array.isArray(item.reviewOptions) ? item.reviewOptions.join("\n") : ""}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({
...current,
reviewOptions: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
}))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Parsed Interpretation</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.parsedInterpretation || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, parsedInterpretation: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Implementation Approach</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea"
value={String(item.implementationApproach || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, implementationApproach: event.target.value }))}
/>
</label>
<label className="launcher-request-admin-field">
<span className="launcher-request-filter-label">Notes</span>
<textarea
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
value={String(item.notes || "")}
onChange={(event) => updateAdminDraftItem(itemIndex, (current) => ({ ...current, notes: event.target.value }))}
/>
</label>
</section>
))}
<div className="launcher-request-admin-actions">
<button
type="button"
className="launcher-primary-btn"
onClick={() => void handleSaveAdminRequest()}
disabled={adminSaving}
>
{adminSaving ? "Saving..." : "Save Review Changes"}
</button>
<button
type="button"
className="launcher-secondary-btn"
onClick={() => void handleApproveAdminRequest()}
disabled={adminSaving}
>
Approve Request
</button>
</div>
</div>
)}
</section>
<section className="launcher-request-admin-card">
<div className="launcher-request-admin-card-head">
<h4 className="launcher-request-admin-card-title">Recent Logs</h4>
@ -997,6 +1551,7 @@ function WorldshaperLauncher() {
const isActiveRequest = requestEntry.status === "active";
const requestDisplayState = getRequestDisplayStateLabel(requestEntry);
const requestDisplayStateClassName = getRequestDisplayStateClassName(requestEntry);
const reviewItem = getPrimaryAnalysisItem(requestEntry);
const analysisStateLabel = requestEntry.status === "pending"
? formatAnalysisStateLabel(requestEntry.analysis?.state)
: "";
@ -1027,6 +1582,11 @@ function WorldshaperLauncher() {
<div className="launcher-request-entry-text">
{requestEntry.status === "active" ? requestEntry.summary : requestEntry.sourceText}
</div>
{requestEntry.status === "pending" && reviewItem?.reviewRationale ? (
<div className="launcher-request-entry-review-note">
Review reason: {reviewItem.reviewRationale}
</div>
) : null}
<div className="launcher-request-entry-meta">
{requestEntry.status === "pending" ? `${analysisStateLabel} | ` : ""}
{formatRequestTimestamp(requestEntry.updatedAt || requestEntry.createdAt)}

View file

@ -448,6 +448,12 @@ body {
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 10px;
cursor: pointer;
}
.launcher-request-admin-request-row.is-selected {
border-color: #63a6f2;
box-shadow: 0 0 0 1px rgba(99, 166, 242, 0.45);
}
.launcher-request-admin-request-copy {
@ -456,6 +462,12 @@ body {
gap: 5px;
}
.launcher-request-admin-request-rationale {
color: #d7e7ff;
font-size: 12px;
line-height: 1.45;
}
.launcher-request-admin-request-title,
.launcher-request-admin-log-title {
color: #eef6ff;
@ -500,6 +512,65 @@ body {
white-space: pre-wrap;
}
.launcher-request-admin-editor-card {
grid-column: 1 / -1;
}
.launcher-request-admin-editor {
display: grid;
gap: 12px;
min-height: 0;
overflow: auto;
max-height: 58dvh;
padding-right: 4px;
}
.launcher-request-admin-editor-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.launcher-request-admin-tag-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.launcher-request-admin-tag-toggle {
cursor: pointer;
}
.launcher-request-admin-tag-toggle.is-active {
border-color: #4fa87e;
background: rgba(19, 73, 50, 0.88);
color: #b7f0d5;
}
.launcher-request-admin-analysis-item {
display: grid;
gap: 10px;
padding: 12px;
border: 1px solid #365782;
border-radius: 12px;
background: rgba(8, 16, 31, 0.72);
}
.launcher-request-admin-analysis-head {
display: flex;
justify-content: space-between;
align-items: start;
gap: 10px;
}
.launcher-request-admin-textarea {
min-height: 96px;
}
.launcher-request-admin-textarea-sm {
min-height: 74px;
}
.launcher-request-composer-label {
color: #d7e7ff;
font-size: 12px;
@ -680,6 +751,12 @@ body {
white-space: pre-wrap;
}
.launcher-request-entry-review-note {
color: #ffd5b0;
font-size: 12px;
line-height: 1.45;
}
.launcher-request-entry-meta,
.launcher-request-empty {
color: #9fb8e5;
@ -1934,6 +2011,15 @@ button.danger:not(:disabled):hover {
grid-template-columns: 1fr;
}
.launcher-request-admin-editor-grid {
grid-template-columns: 1fr;
}
.launcher-request-admin-analysis-head {
grid-template-columns: 1fr;
display: grid;
}
.launcher-request-filter {
min-width: 0;
}