305 lines
14 KiB
HTML
305 lines
14 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>
|
|
|
|
<div class="modal fade" id="photoModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="modalUserName">User Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<img src="" id="modalImage" class="user-photo me-3" style="width: 80px; height: 80px;">
|
|
<div>
|
|
<h4 id="modalUserName" class="mb-0"></h4>
|
|
<small id="modalSession" class="text-muted"></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Pet Details</h6>
|
|
<table class="table table-sm border">
|
|
<tr><td>Pet:</td><td id="modalPetName" class="fw-bold"></td></tr>
|
|
<tr><td>Type:</td><td id="modalPetType"></td></tr>
|
|
<tr><td>Vibe:</td><td id="modalVibe"></td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Assets</h6>
|
|
<div id="modalMediaContainer">
|
|
<!-- We will inject an <audio> or <video> tag here via JS -->
|
|
</div>
|
|
</div>
|
|
<div class="col-md-12 mt-3">
|
|
<h6>Lyrics</h6>
|
|
<pre id="modalLyrics" class="bg-dark text-white p-3 rounded small" style="max-height: 150px; overflow-y: auto;"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.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>
|
|
$(document).ready(function() {
|
|
const API_BASE_URL = '';
|
|
const API_ENDPOINT = `/back/api/admin/data`;
|
|
|
|
// Helper to convert container paths to web URLs
|
|
function fixStoragePath(path) {
|
|
if (!path) return '';
|
|
return path.replace(/^\/app\/storage\//, '/storage/');
|
|
}
|
|
|
|
// 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,
|
|
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' },
|
|
{ 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) => `<img src="${fixStoragePath(data)}" 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: 'Video', defaultContent: '', render: d => d ? '🎥' : '-' },
|
|
{
|
|
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
|
|
$('#modalUserName').text(rowData.owner_name + ' - Profile');
|
|
$('#modalImage').attr('src', fixStoragePath(rowData.photo_path));
|
|
$('#modalPetName').text(rowData.pet_name || 'N/A');
|
|
$('#modalPetType').text(rowData.pet_type || 'N/A');
|
|
$('#modalVibe').text(rowData.music_vibe || 'N/A');
|
|
$('#modalSession').text(rowData.session_id);
|
|
// 3. Handle Lyrics
|
|
if (rowData.lyrics) {
|
|
$('#modalLyrics').text(rowData.lyrics);
|
|
} else {
|
|
$('#modalLyrics').text('No lyrics available yet...');
|
|
}
|
|
|
|
// 4. Handle Media (Song or Video)
|
|
const mediaContainer = $('#modalMediaContainer');
|
|
mediaContainer.empty(); // Clear previous content
|
|
|
|
if (rowData.generated_video_path) {
|
|
mediaContainer.html(`
|
|
<video controls class="w-100" style="max-height: 300px;">
|
|
<source src="${fixStoragePath(rowData.generated_video_path)}" type="video/mp4">
|
|
Your browser does not support video.
|
|
</video>
|
|
`);
|
|
} else if (rowData.generated_song_path) {
|
|
mediaContainer.html(`
|
|
<audio controls class="w-100">
|
|
<source src="${fixStoragePath(rowData.generated_song_path)}" type="audio/mpeg">
|
|
Your browser does not support audio.
|
|
</audio>
|
|
`);
|
|
} else {
|
|
mediaContainer.html('<p class="text-muted">No media available yet...</p>');
|
|
}
|
|
|
|
// 5. Show the Modal
|
|
new bootstrap.Modal('#photoModal').show();
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|