visionvortex-watcher / index.html
Egrigor's picture
how to ensure the video renders beside the chat data column not above it?
93085bb verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>YOLO Webcam + Chat</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body class="bg-dark text-white">
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4>Welcome, {{ username }}</h4>
<a href="/logout" class="btn btn-outline-light btn-sm">Logout</a>
</div>
<div class="row">
<!-- Left Column - Webcam -->
<div class="col-md-8 mb-4">
<div class="text-center">
<h5>Live Webcam</h5>
<img src="{{ url_for('video_feed') }}" class="img-fluid rounded" style="max-height: 600px; width: 100%;">
</div>
</div>
<!-- Right Column - Highlights and Chat -->
<div class="col-md-4 mb-4">
<div class="sticky-top" style="top: 20px;">
<!-- Biome Display -->
<div class="mb-3 p-2 bg-info bg-opacity-25 rounded text-center">
<strong>Current Biome:</strong> <span id="current-biome">Loading...</span>
</div>
<!-- Detection Timeline -->
<div class="mb-4">
<h5>Detection Highlights</h5>
<div id="categorized-timeline" class="bg-secondary rounded p-3" style="max-height: 300px; overflow-y: auto;">
<p class="text-muted">Loading detection events...</p>
</div>
</div>
<!-- Group Chat - Moved here -->
<div class="mb-4">
<h5>Group Chat</h5>
<div id="chatbox" class="bg-secondary rounded p-3 mb-3" style="height: 200px; overflow-y: scroll;"></div>
<div class="input-group">
<input id="message" class="form-control" placeholder="Type a message...">
<button class="btn btn-primary" onclick="sendMessage()">Send</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// === Chat Functions ===
function loadChat() {
$.get("/chat", function(data) {
let html = "";
data.forEach(msg => {
html += `<p><strong>${msg.user}</strong>: ${msg.message}<br><small class="text-muted">${msg.timestamp}</small></p>`;
});
$("#chatbox").html(html).scrollTop(9999);
});
}
function sendMessage() {
$.post("/chat", {message: $("#message").val()}, function() {
$("#message").val("");
loadChat();
});
}
// === Detection Timeline (Categorized) ===
function loadTimeline() {
$.get("/detections", function(response) {
// Update current biome
const biome = response.current_biome || "Unknown";
// Format biome name: "biome-meadows" β†’ "Meadows"
const formattedBiome = biome
.replace(/^biome-/, '')
.replace(/-/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
$("#current-biome").text(formattedBiome);
// Render categorized events
const grouped = response.events || {};
const categoryTitles = {
"status": "Active Status Effects",
"recent_buff": "Recent Consumables",
"boss_ability": "Boss Powers",
"boss_health": "Boss Health",
"creature": "Creatures Nearby",
"crafting": "Crafting Stations",
"plant": "Plants & Crops",
"storage": "Storage",
"vehicle": "Vehicles",
"world": "World Events",
"other": "Other"
};
const renderOrder = [
"status", "recent_buff", "boss_ability", "boss_health",
"creature", "crafting", "plant", "storage", "vehicle", "world", "other"
];
let html = "";
let hasContent = false;
renderOrder.forEach(cat => {
const events = grouped[cat];
if (events && events.length > 0) {
hasContent = true;
const title = categoryTitles[cat] || cat.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
html += `<h6 class="mt-3">${title}</h6><div class="ps-2">`;
events.forEach(e => {
const conf = (e.confidence * 100).toFixed(0);
let badgeColor = "secondary";
if (cat === "status") badgeColor = "warning";
else if (cat === "boss_health") badgeColor = "danger";
else if (cat === "creature") badgeColor = "success";
else if (cat === "recent_buff") badgeColor = "primary";
else if (cat === "boss_ability") badgeColor = "dark";
else if (cat === "world") badgeColor = "info";
html += `<p class="mb-1"><span class="badge bg-${badgeColor}">${e.class}</span> ${conf}%</p>`;
});
html += `</div>`;
}
});
$("#categorized-timeline").html(
hasContent ? html : "<p class='text-muted'>No active detections.</p>"
);
}).fail(() => {
$("#current-biome").text("Offline");
$("#categorized-timeline").html("<p class='text-muted'>Failed to load detections.</p>");
});
}
// === Polling ===
setInterval(loadChat, 3000);
setInterval(loadTimeline, 2000);
// Initial loads
loadChat();
loadTimeline();
</script>
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
</body>
</html>