361 lines
18 KiB
HTML
361 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin</title>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/2.3.6/css/dataTables.dataTables.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/3.0.7/css/responsive.dataTables.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/3.0.2/css/buttons.dataTables.min.css">
|
|
<link href="https://cdn.datatables.net/columncontrol/1.2.0/css/columnControl.dataTables.min.css" rel="stylesheet">
|
|
|
|
<style>
|
|
body { background-color: #f8f9fa; }
|
|
.user-photo {
|
|
width: 45px;
|
|
height: 45px;
|
|
object-fit: cover;
|
|
border-radius: 10%;
|
|
border: 1px solid #ddd;
|
|
}
|
|
/* Style adjustments for DataTables 2.0 + Bootstrap */
|
|
.dt-container { margin-top: 20px; padding: 5px; }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<nav class="navbar navbar-dark bg-dark shadow-sm mb-4">
|
|
<div class="container">
|
|
<span class="navbar-brand">Admin Dashboard</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container mb-5">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3 mb-3">
|
|
<!-- Left Column: Total Records -->
|
|
<div class="col-md-6">
|
|
<div class="alert alert-light mb-0 h-100 d-flex align-items-center">
|
|
<span>
|
|
Total Records:
|
|
<span id="total-records-count" class="badge bg-dark rounded-pill">0</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: AI Credits -->
|
|
<div class="col-md-6">
|
|
<div class="alert alert-light mb-0 h-100 d-flex align-items-center">
|
|
<span>
|
|
SonautoAI Credits Remaining:
|
|
<span id="credits-remaining" class="badge bg-primary rounded-pill">Loading...</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card shadow-sm dt-container">
|
|
<div class="card-dt">
|
|
<table id="usersTable" class="table table-striped w-100">
|
|
<thead>
|
|
<tr>
|
|
<th>Session</th>
|
|
<th>Created</th>
|
|
<th>Cookie ID</th>
|
|
<th>Owner Name</th>
|
|
<th>Pet Name</th>
|
|
<th>Photo</th>
|
|
<th>Pet Type</th>
|
|
<th>Vibe</th>
|
|
<th>Retries</th>
|
|
<th>Sent to LLM</th>
|
|
<th>Task ID</th>
|
|
<th>Recv from LLM</th>
|
|
<th>LLM Response</th>
|
|
<th>Full Response</th>
|
|
<th>Song Path</th>
|
|
<th>LLM Status</th>
|
|
<th>Lyrics</th>
|
|
<th>Vid Start</th>
|
|
<th>Vid End</th>
|
|
<th>Video Path</th>
|
|
<th>Entry Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody> <!-- Empty: DataTables will fill this -->
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 1. Use modal-xl for desktop real estate -->
|
|
<div class="modal fade" id="photoModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Record Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<!-- Left Column: Media (Fixed Width for 9:16) -->
|
|
<div class="col-md-4 border-end">
|
|
<h6>Original Photo</h6>
|
|
<img src="" id="modalImage" class="img-thumbnail mb-3"
|
|
style="width: 150px; height: 150px; object-fit: cover;">
|
|
|
|
<h6>Generated Media</h6>
|
|
<div id="modalMediaContainer" class="bg-black rounded overflow-hidden shadow-sm"
|
|
style="max-width: 280px; min-height: 200px;">
|
|
<!-- Video/Audio injected here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Full Data Table -->
|
|
<div class="col-md-8">
|
|
<h6>Full Entry Metadata</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover border">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Field</th>
|
|
<th>Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modalDetailsTableBody">
|
|
<!-- Dynamic content -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h6 class="mt-4">Lyrics</h6>
|
|
|
|
<pre id="modalLyrics" class="bg-dark text-info p-3 rounded"
|
|
style="white-space: pre-wrap; font-size: 0.85rem; max-height: 300px; overflow-y: auto;">
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script defer src="./assets/vendor/alpine.min.js"></script>
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.datatables.net/2.3.6/js/dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/responsive/3.0.7/js/dataTables.responsive.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/3.0.2/js/dataTables.buttons.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.colVis.min.js"></script>
|
|
<script src="https://cdn.datatables.net/columncontrol/1.2.0/js/dataTables.columnControl.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3.7.2/build/global/luxon.min.js"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
const API_BASE_URL = '';
|
|
const API_ENDPOINT = `/back/api/admin/data`;
|
|
const MEDIA_BASE_URL = 'https://storage.googleapis.com/vday2026/';
|
|
|
|
// Helper to convert container paths to web URLs
|
|
function fixStoragePath(path) {
|
|
if (!path) return '';
|
|
// let relative_path = path.replace(/^\/app\/storage\//, '/storage/');
|
|
const isRelative = path.startsWith('uploads/') || path.startsWith('audio/') || path.startsWith('video/');
|
|
return isRelative ? MEDIA_BASE_URL + path : path;
|
|
}
|
|
|
|
// Fetch and display Sonauto credits
|
|
function loadCredits() {
|
|
fetch('/back/api/admin/queue-status')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
$('#credits-remaining').text((data.sonauto_credits || 0).toLocaleString());
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to load credits:', err);
|
|
$('#credits-remaining').text('Error');
|
|
});
|
|
}
|
|
loadCredits();
|
|
|
|
// Initialize DataTable
|
|
const table = $('#usersTable').DataTable({
|
|
serverSide: true,
|
|
processing: true,
|
|
order: [[1, 'desc']],
|
|
responsive: {
|
|
details: {
|
|
type: 'column',
|
|
target: 'tr' // Clicking the row expands it
|
|
}
|
|
},
|
|
// Define which columns are visible by default
|
|
columnDefs: [
|
|
{ responsivePriority: 1, targets: [3, 4, 12, 5] }, // Owner, Pet, Status, Photo
|
|
{ responsivePriority: 2, targets: [1, 20] }, // CreatedAt, EntryStatus
|
|
{ targets: [2, 6, 8, 9, 10, 11, 14, 17, 18], visible: false } // Hide technical IDs by default
|
|
],
|
|
ajax: {
|
|
url: API_ENDPOINT,
|
|
dataSrc: function (json) {
|
|
// Your FastAPI endpoint returns AdminResponse directly
|
|
// Structure: { draw, recordsTotal, recordsFiltered, data }
|
|
|
|
// console.log('DEBUG: API Response:', json);
|
|
|
|
// Update the total records badge at the top
|
|
$('#total-records-count').text((json.recordsTotal || 0).toLocaleString());
|
|
|
|
// DataTables expects these fields at root level (already there!)
|
|
// No need to move anything - just return the data array
|
|
return json.data || [];
|
|
}
|
|
},
|
|
layout: {
|
|
topStart: {
|
|
buttons: [
|
|
{
|
|
extend: 'colvis',
|
|
text: 'Select Columns',
|
|
className: 'btn btn-sm btn-outline-secondary'
|
|
}
|
|
]
|
|
},
|
|
topEnd: 'search',
|
|
bottomStart: 'info',
|
|
bottomEnd: 'paging'
|
|
},
|
|
columns: [
|
|
{ data: 'session_id', title: 'Session', defaultContent: 'N/A' },
|
|
{
|
|
data: 'created_at',
|
|
title: 'Created',
|
|
defaultContent: 'N/A',
|
|
render: (data) => {
|
|
if (!data) return 'N/A';
|
|
return luxon.DateTime.fromISO(data).toFormat('yyyy-MM-dd HH:mm:ss') + ' UTC';
|
|
}
|
|
},
|
|
{ data: 'cookie_id', title: 'Cookie ID', defaultContent: 'N/A' },
|
|
{ data: 'owner_name', title: 'Owner', defaultContent: 'N/A' },
|
|
{ data: 'pet_name', title: 'Pet Name', defaultContent: 'N/A' },
|
|
{
|
|
data: 'photo_path',
|
|
title: 'Photo',
|
|
defaultContent: '',
|
|
render: (data) => {
|
|
const imgPath = fixStoragePath(data) || "https://placehold.co/500x500/fbba0e/e11d48/jpg?text=No\nImage";
|
|
return `<img src="${imgPath}" class="user-photo img-preview" style="cursor:pointer">`;
|
|
}
|
|
},
|
|
{ data: 'pet_type', title: 'Type', defaultContent: 'N/A' },
|
|
{ data: 'music_vibe', title: 'Vibe', defaultContent: 'N/A' },
|
|
{ data: 'retry_count', title: 'Retries', defaultContent: 'N/A' },
|
|
{ data: 'sent_to_LLM', title: 'Sent to LLM', defaultContent: 'N/A' },
|
|
{ data: 'LLM_task_id', title: 'Task ID', defaultContent: 'N/A' },
|
|
{ data: 'received_from_LLM', title: 'LLM Recv', defaultContent: 'N/A' },
|
|
{
|
|
data: 'LLM_response',
|
|
title: 'LLM Response',
|
|
defaultContent: 'N/A',
|
|
render: (data) => data ? (data.substring(0, 30) + '...') : 'Pending'
|
|
},
|
|
{ data: 'LLM_full_response', defaultContent: 'N/A', visible: false },
|
|
{ data: 'generated_song_path', title: 'Song', defaultContent: '', render: d => d ? '🎵' : '-' },
|
|
{ data: 'LLM_status', title: 'LLM Status', defaultContent: 'N/A' },
|
|
{ data: 'lyrics', title: 'Lyrics', defaultContent: '', visible: false },
|
|
{ data: 'video_creation_start', title: 'Vid Start', defaultContent: '', visible: false },
|
|
{ data: 'video_creation_end', title: 'Vid End', defaultContent: '', visible: false },
|
|
{ data: 'generated_video_path', title: 'VideoGen', defaultContent: '', render: d => d ? 'Yes' : 'No' },
|
|
{
|
|
data: 'entry_status',
|
|
title: 'Status',
|
|
defaultContent: '',
|
|
render: (data) => {
|
|
return `<span>${data}</span>`;
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
// Use event delegation (so it works even after searching/paging)
|
|
$('#usersTable').on('click', '.img-preview', function() {
|
|
// 1. Get the data for the specific row clicked
|
|
// We use .closest('tr') to find the table row containing the clicked image
|
|
const rowData = table.row($(this).closest('tr')).data();
|
|
|
|
// 2. Populate the Modal fields
|
|
$('.modal-title').text(`Session: ${rowData.session_id}`);
|
|
$('#modalImage').attr('src', fixStoragePath(rowData.photo_path) || "https://placehold.co/500x500/fbba0e/e11d48/jpg?text=No\nImage");
|
|
|
|
// 3. Dynamic Table Generation for all 20+ columns
|
|
const tableBody = $('#modalDetailsTableBody');
|
|
tableBody.empty();
|
|
// List of keys we want to display or skip
|
|
Object.entries(rowData).forEach(([key, value]) => {
|
|
// Skip keys that are handled visually (media/lyrics)
|
|
if (['photo_path', 'generated_video_path', 'generated_song_path', 'lyrics'].includes(key))
|
|
return;
|
|
|
|
let displayValue = value ?? '<span class="text-muted italic">null</span>';
|
|
// Surgical Link Detection: Only link if it has a known file extension
|
|
const isFile = typeof value === 'string' && /\.(jpg|jpeg|png|mp3|mp4|wav)$/i.test(value);
|
|
if (isFile) {
|
|
const fullUrl = fixStoragePath(value);
|
|
displayValue = `<a href="${fullUrl}" target="_blank" class="text-decoration-none text-truncate d-inline-block" style="max-width: 300px;">${value} 🔗</a>`;
|
|
}
|
|
tableBody.append(`
|
|
<tr>
|
|
<td class="fw-bold text-muted" style="width: 30%">${key.replace(/_/g, ' ')}</td>
|
|
<td class="text-break">${displayValue}</td>
|
|
</tr>
|
|
`);
|
|
});
|
|
|
|
$('#modalLyrics').text(rowData.lyrics || 'No lyrics generated.');
|
|
|
|
|
|
// 4. Handle Media (Song or Video)
|
|
const mediaContainer = $('#modalMediaContainer');
|
|
|
|
console.group(`--- Media Debug: Session ${rowData.session_id} ---`);
|
|
const videoUrl = fixStoragePath(rowData.generated_video_path);
|
|
const audioUrl = fixStoragePath(rowData.generated_song_path);
|
|
console.log('Video URL:', videoUrl);
|
|
console.log('Audio URL:', audioUrl);
|
|
console.groupEnd();
|
|
|
|
// Reset container state
|
|
mediaContainer.empty().removeClass('ratio ratio-9x16 d-flex align-items-center p-3');
|
|
|
|
if (videoUrl && rowData.generated_video_path) {
|
|
console.log('Injecting Video...');
|
|
mediaContainer.addClass('ratio ratio-9x16');
|
|
mediaContainer.html(`
|
|
<video controls preload="metadata" class="w-100 h-100" src="${videoUrl}">
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
`);
|
|
} else if (audioUrl && rowData.generated_song_path) {
|
|
console.log('Injecting Audio...');
|
|
mediaContainer.addClass('d-flex align-items-center p-3');
|
|
mediaContainer.html(`
|
|
<audio controls preload="metadata" class="w-100" src="${audioUrl}">
|
|
Your browser does not support audio.
|
|
</audio>
|
|
`);
|
|
} else {
|
|
mediaContainer.html('<div class="d-flex align-items-center justify-content-center h-100 text-white-50">No Media</div>');
|
|
}
|
|
|
|
// 5. Show the Modal
|
|
new bootstrap.Modal('#photoModal').show();
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|