277 lines
12 KiB
HTML
277 lines
12 KiB
HTML
{% extends "admin/layout.html" %}
|
|
{% block admin_title %}{{ t.nav_reviews }}{% endblock admin_title %}
|
|
|
|
{% block content %}
|
|
<h1>{{ t.reviews_heading }}</h1>
|
|
|
|
<div style="margin-bottom: 1rem; display: flex; gap: .5rem; align-items: center; flex-wrap: wrap;">
|
|
<a href="/admin/reviews" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "" %} #333; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_all }}</a>
|
|
<a href="/admin/reviews?status=pending" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "pending" %} #ffc107; color: #000{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_pending }}</a>
|
|
<a href="/admin/reviews?status=approved" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "approved" %} #28a745; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_approved }}</a>
|
|
<a href="/admin/reviews?status=rejected" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "rejected" %} #dc3545; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_rejected }}</a>
|
|
<a href="/admin/reviews?status=queued" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "queued" %} #17a2b8; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_queued }}</a>
|
|
<a href="/admin/reviews?status=processing" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "processing" %} #007bff; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_processing }}</a>
|
|
<a href="/admin/reviews?status=auto_approved" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "auto_approved" %} #28a745; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_auto_approved }}</a>
|
|
<a href="/admin/reviews?status=failed" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "failed" %} #dc3545; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_failed }}</a>
|
|
{% if !rows.is_empty() %}
|
|
<span style="flex:1;"></span>
|
|
<form method="post" action="/admin/reviews/clear{% if !status_filter.is_empty() %}?status={{ status_filter }}{% endif %}" style="margin:0;" onsubmit="return confirm('{{ t.reviews_clear_confirm }}');">
|
|
<button type="submit" style="padding:.3rem .8rem; border-radius:4px; border:1px solid #dc3545; background:#fff; color:#dc3545; cursor:pointer;">{% if status_filter.is_empty() %}{{ t.reviews_clear_all }}{% else %}{{ t.reviews_clear_filtered }}{% endif %}</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if rows.is_empty() %}
|
|
<p>{{ t.reviews_empty }}</p>
|
|
{% else %}
|
|
<form id="reviews-bulk-form" method="post" action="/admin/reviews/bulk" style="margin:0;">
|
|
<input type="hidden" name="selected_ids" id="selected-review-ids" value="">
|
|
<input type="hidden" name="status_filter" value="{{ status_filter }}">
|
|
<div class="review-bulk-toolbar">
|
|
<button type="button" id="select-shown-reviews" class="review-toolbar-button">{{ t.reviews_select_all }}</button>
|
|
<button type="button" id="clear-review-selection" class="review-toolbar-button">{{ t.reviews_clear_selection }}</button>
|
|
<button type="submit" name="action" value="delete" class="review-danger-button" disabled>{{ t.reviews_delete_selected }}</button>
|
|
<button type="submit" name="action" value="requeue" class="review-primary-button" disabled>{{ t.reviews_requeue_selected }}</button>
|
|
<span id="review-selection-summary" class="review-selection-summary">{{ t.reviews_selected_none }}</span>
|
|
</div>
|
|
<table>
|
|
<tr>
|
|
<th class="review-select-cell"></th>
|
|
<th>ID</th>
|
|
<th>{{ t.reviews_status }}</th>
|
|
<th>{{ t.reviews_type }}</th>
|
|
<th>{{ t.reviews_input_path }}</th>
|
|
<th>{{ t.reviews_tags }}</th>
|
|
<th>{{ t.reviews_confidence }}</th>
|
|
<th>{{ t.reviews_model }}</th>
|
|
<th>{{ t.reviews_llm_duration }}</th>
|
|
<th>{{ t.reviews_tokens }}</th>
|
|
<th>{{ t.reviews_created }}</th>
|
|
<th>{{ t.jobs_actions }}</th>
|
|
</tr>
|
|
{% for row in rows %}
|
|
<tr>
|
|
<td class="review-select-cell">
|
|
<input type="checkbox" class="review-select" value="{{ row.review.id_val() }}" data-status="{{ row.review.status_str() }}" aria-label="Select review {{ row.review.id_val() }}">
|
|
</td>
|
|
<td><a href="/admin/reviews/{{ row.review.id_val() }}">{{ row.review.id_val() }}</a></td>
|
|
<td><span class="badge {{ row.review.status_badge_class() }}">{{ row.review.status_str() }}</span></td>
|
|
<td>{{ row.review.review_type_str() }}</td>
|
|
<td class="review-input-path" title="{{ row.review.input_path_str() }}">{{ row.display_input_path }}</td>
|
|
<td class="review-tag-cell">
|
|
{% for tag in row.media_tags %}
|
|
<span class="review-tag review-tag-{{ tag.kind }}">{{ tag.label }}</span>
|
|
{% endfor %}
|
|
</td>
|
|
<td>{% match row.review.confidence() %}{% when Some with (c) %}{{ c }}{% when None %}-{% endmatch %}</td>
|
|
{% match stats_map.get(&row.review.id_val()) %}
|
|
{% when Some with (s) %}
|
|
<td>{{ s.model_name }}</td>
|
|
<td>{{ s.duration_display() }}</td>
|
|
<td>{{ s.tokens_display() }}</td>
|
|
{% when None %}
|
|
<td>-</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
{% endmatch %}
|
|
<td>{{ row.review.created_at_str() }}</td>
|
|
<td>
|
|
<a href="/admin/reviews/{{ row.review.id_val() }}">{{ t.reviews_view }}</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</table>
|
|
</form>
|
|
{% endif %}
|
|
|
|
<style>
|
|
.review-bulk-toolbar {
|
|
margin-bottom: .75rem;
|
|
display: flex;
|
|
gap: .5rem;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
.review-toolbar-button,
|
|
.review-danger-button,
|
|
.review-primary-button {
|
|
padding: .35rem .7rem;
|
|
border-radius: 4px;
|
|
border: 1px solid #ced4da;
|
|
background: #fff;
|
|
color: #212529;
|
|
cursor: pointer;
|
|
}
|
|
.review-danger-button {
|
|
border-color: #dc3545;
|
|
color: #dc3545;
|
|
}
|
|
.review-primary-button {
|
|
border-color: #17a2b8;
|
|
color: #0c5460;
|
|
}
|
|
.review-danger-button:disabled,
|
|
.review-primary-button:disabled {
|
|
cursor: not-allowed;
|
|
opacity: .45;
|
|
}
|
|
.review-selection-summary {
|
|
min-height: 1.7rem;
|
|
padding: .35rem .6rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
background: #f8f9fa;
|
|
color: #495057;
|
|
font-size: .9rem;
|
|
white-space: nowrap;
|
|
}
|
|
.review-select-cell {
|
|
width: 2.25rem;
|
|
text-align: center;
|
|
}
|
|
.review-select {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
}
|
|
.review-input-path {
|
|
max-width: 34rem;
|
|
white-space: normal;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.review-tag-cell {
|
|
max-width: 18rem;
|
|
}
|
|
.review-tag {
|
|
display: inline-block;
|
|
margin: .1rem .15rem .1rem 0;
|
|
padding: .12rem .35rem;
|
|
border: 1px solid #ced4da;
|
|
border-radius: 4px;
|
|
background: #f8f9fa;
|
|
color: #495057;
|
|
font-size: .8rem;
|
|
white-space: nowrap;
|
|
}
|
|
.review-tag-format { border-color: #9ec5fe; background: #e7f1ff; color: #084298; }
|
|
.review-tag-bitrate { border-color: #a3cfbb; background: #d1e7dd; color: #0f5132; }
|
|
.review-tag-sample { border-color: #ffda6a; background: #fff3cd; color: #664d03; }
|
|
.review-tag-depth { border-color: #d0bfff; background: #f0e7ff; color: #3d246c; }
|
|
.review-tag-size { border-color: #ced4da; background: #f8f9fa; color: #495057; }
|
|
.badge-completed { background: #d4edda; color: #155724; }
|
|
.badge-failed { background: #f8d7da; color: #721c24; }
|
|
.badge-pending { background: #fff3cd; color: #856404; }
|
|
.badge-queued { background: #d1ecf1; color: #0c5460; }
|
|
.badge-processing { background: #cce5ff; color: #004085; }
|
|
</style>
|
|
|
|
<script>
|
|
(() => {
|
|
const form = document.getElementById("reviews-bulk-form");
|
|
if (!form) {
|
|
return;
|
|
}
|
|
|
|
const checkboxes = Array.from(form.querySelectorAll(".review-select"));
|
|
const selectedIdsInput = document.getElementById("selected-review-ids");
|
|
const summary = document.getElementById("review-selection-summary");
|
|
const selectShownButton = document.getElementById("select-shown-reviews");
|
|
const clearSelectionButton = document.getElementById("clear-review-selection");
|
|
const submitButtons = Array.from(form.querySelectorAll("button[type='submit']"));
|
|
const selected = new Set();
|
|
const statusCounts = new Map();
|
|
|
|
const labels = {
|
|
pending: "{{ t.reviews_filter_pending }}",
|
|
approved: "{{ t.reviews_filter_approved }}",
|
|
rejected: "{{ t.reviews_filter_rejected }}",
|
|
queued: "{{ t.reviews_filter_queued }}",
|
|
processing: "{{ t.reviews_filter_processing }}",
|
|
auto_approved: "{{ t.reviews_filter_auto_approved }}",
|
|
failed: "{{ t.reviews_filter_failed }}"
|
|
};
|
|
|
|
function setStatusCount(status, delta) {
|
|
const next = (statusCounts.get(status) || 0) + delta;
|
|
if (next > 0) {
|
|
statusCounts.set(status, next);
|
|
} else {
|
|
statusCounts.delete(status);
|
|
}
|
|
}
|
|
|
|
function syncControls() {
|
|
selectedIdsInput.value = Array.from(selected).join(",");
|
|
const total = selected.size;
|
|
for (const button of submitButtons) {
|
|
button.disabled = total === 0;
|
|
}
|
|
if (total === 0) {
|
|
summary.textContent = "{{ t.reviews_selected_none }}";
|
|
return;
|
|
}
|
|
const parts = Array.from(statusCounts.entries())
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([status, count]) => `${labels[status] || status}: ${count}`);
|
|
summary.textContent = `{{ t.reviews_selected_prefix }}: ${total} (${parts.join(", ")})`;
|
|
}
|
|
|
|
function setChecked(checkbox, checked) {
|
|
const id = checkbox.value;
|
|
const isSelected = selected.has(id);
|
|
checkbox.checked = checked;
|
|
if (isSelected === checked) {
|
|
return;
|
|
}
|
|
const status = checkbox.dataset.status || "unknown";
|
|
if (checked) {
|
|
selected.add(id);
|
|
setStatusCount(status, 1);
|
|
} else {
|
|
selected.delete(id);
|
|
setStatusCount(status, -1);
|
|
}
|
|
}
|
|
|
|
for (const checkbox of checkboxes) {
|
|
checkbox.addEventListener("change", () => {
|
|
setChecked(checkbox, checkbox.checked);
|
|
syncControls();
|
|
});
|
|
}
|
|
|
|
selectShownButton.addEventListener("click", () => {
|
|
for (const checkbox of checkboxes) {
|
|
setChecked(checkbox, true);
|
|
}
|
|
syncControls();
|
|
});
|
|
|
|
clearSelectionButton.addEventListener("click", () => {
|
|
for (const checkbox of checkboxes) {
|
|
setChecked(checkbox, false);
|
|
}
|
|
syncControls();
|
|
});
|
|
|
|
form.addEventListener("submit", (event) => {
|
|
syncControls();
|
|
if (selected.size === 0) {
|
|
event.preventDefault();
|
|
alert("{{ t.reviews_none_selected_confirm }}");
|
|
return;
|
|
}
|
|
|
|
const action = event.submitter ? event.submitter.value : "";
|
|
const message = action === "requeue"
|
|
? "{{ t.reviews_requeue_selected_confirm }}"
|
|
: "{{ t.reviews_delete_selected_confirm }}";
|
|
if (!confirm(message)) {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
syncControls();
|
|
})();
|
|
</script>
|
|
{% endblock content %}
|