bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
307 lines
12 KiB
HTML
307 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>QUIXZOOM Human-in-the-Loop Annotation</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, system-ui, sans-serif; background: #0a0e27; color: #fff; height: 100vh; display: flex; }
|
|
|
|
/* Sidebar */
|
|
.sidebar { width: 320px; background: #1a1f3a; border-right: 1px solid #2a3050; display: flex; flex-direction: column; }
|
|
.sidebar-header { padding: 16px; border-bottom: 1px solid #2a3050; }
|
|
.sidebar-header h1 { font-size: 16px; margin-bottom: 4px; }
|
|
.sidebar-header p { font-size: 12px; color: #8b92b4; }
|
|
|
|
/* AI Proposals */
|
|
.proposals { flex: 1; overflow-y: auto; padding: 12px; }
|
|
.proposal { background: #2a3050; border-radius: 8px; padding: 12px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
|
|
.proposal:hover { background: #3a4060; }
|
|
.proposal.selected { border: 2px solid #4ade80; }
|
|
.proposal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
.proposal-type { font-size: 14px; font-weight: 600; }
|
|
.proposal-confidence { font-size: 12px; padding: 2px 8px; border-radius: 4px; }
|
|
.confidence-high { background: #4ade8020; color: #4ade80; }
|
|
.confidence-medium { background: #fbbf2420; color: #fbbf24; }
|
|
.confidence-low { background: #f8717120; color: #f87171; }
|
|
.proposal-meta { font-size: 11px; color: #8b92b4; }
|
|
.proposal-models { display: flex; gap: 4px; margin-top: 6px; }
|
|
.model-tag { font-size: 10px; padding: 2px 6px; border-radius: 3px; background: #3a4060; color: #c5c9e0; }
|
|
|
|
/* Actions */
|
|
.proposal-actions { display: flex; gap: 4px; margin-top: 8px; }
|
|
.action-btn { flex: 1; padding: 6px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; }
|
|
.btn-approve { background: #4ade80; color: #0a0e27; }
|
|
.btn-reject { background: #f87171; color: #fff; }
|
|
.btn-correct { background: #fbbf24; color: #0a0e27; }
|
|
|
|
/* Stats */
|
|
.stats { padding: 12px; border-top: 1px solid #2a3050; }
|
|
.stat-row { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 12px; }
|
|
.stat-value { font-weight: 600; }
|
|
|
|
/* Main Content */
|
|
.main { flex: 1; display: flex; flex-direction: column; }
|
|
.toolbar { height: 48px; background: #1a1f3a; border-bottom: 1px solid #2a3050; display: flex; align-items: center; padding: 0 16px; gap: 12px; }
|
|
.tool-btn { padding: 6px 12px; border: none; border-radius: 4px; background: #2a3050; color: #fff; cursor: pointer; font-size: 13px; }
|
|
.tool-btn.active { background: #4ade80; color: #0a0e27; }
|
|
.tool-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
/* Canvas Area */
|
|
.canvas-area { flex: 1; display: flex; align-items: center; justify-content: center; background: #0a0e27; position: relative; overflow: hidden; }
|
|
#annotationCanvas { max-width: 100%; max-height: 100%; cursor: crosshair; }
|
|
|
|
/* UKG Context */
|
|
.ukg-context { position: absolute; top: 16px; right: 16px; background: #1a1f3a; border-radius: 8px; padding: 12px; width: 240px; }
|
|
.ukg-context h3 { font-size: 12px; text-transform: uppercase; color: #8b92b4; margin-bottom: 8px; }
|
|
.ukg-item { font-size: 11px; padding: 4px 0; border-bottom: 1px solid #2a3050; }
|
|
.ukg-item:last-child { border-bottom: none; }
|
|
|
|
/* Navigation */
|
|
.navigation { height: 48px; background: #1a1f3a; border-top: 1px solid #2a3050; display: flex; align-items: center; padding: 0 16px; gap: 12px; }
|
|
.nav-btn { padding: 6px 16px; border: none; border-radius: 4px; background: #2a3050; color: #fff; cursor: pointer; }
|
|
.nav-btn.primary { background: #4ade80; color: #0a0e27; }
|
|
.progress { flex: 1; text-align: center; font-size: 13px; }
|
|
|
|
/* Hierarchy Selector */
|
|
.hierarchy { position: absolute; bottom: 64px; left: 16px; background: #1a1f3a; border-radius: 8px; padding: 12px; display: none; }
|
|
.hierarchy.show { display: block; }
|
|
.hierarchy-item { padding: 6px 12px; cursor: pointer; font-size: 13px; }
|
|
.hierarchy-item:hover { background: #2a3050; }
|
|
.hierarchy-item.level-1 { font-weight: 600; }
|
|
.hierarchy-item.level-2 { padding-left: 24px; }
|
|
.hierarchy-item.level-3 { padding-left: 42px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">
|
|
<h1>AI Förslag</h1>
|
|
<p>Granska och godkänn</p>
|
|
</div>
|
|
|
|
<div class="proposals" id="proposals">
|
|
<!-- Fylls dynamiskt -->
|
|
</div>
|
|
|
|
<div class="stats">
|
|
<div class="stat-row">
|
|
<span>Totalt:</span>
|
|
<span class="stat-value" id="stat-total">0</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Godkända:</span>
|
|
<span class="stat-value" id="stat-approved">0</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Avvisade:</span>
|
|
<span class="stat-value" id="stat-rejected">0</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span>Korrigerade:</span>
|
|
<span class="stat-value" id="stat-corrected">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main">
|
|
<div class="toolbar">
|
|
<button class="tool-btn active" onclick="setTool('select')">Välj</button>
|
|
<button class="tool-btn" onclick="setTool('box')">Rita box</button>
|
|
<button class="tool-btn" onclick="setTool('point')">Punkt</button>
|
|
<button class="tool-btn" onclick="toggleHierarchy()">Hierarki</button>
|
|
<button class="tool-btn" onclick="loadAIProposals()">Ladda AI-förslag</button>
|
|
<button class="tool-btn" onclick="exportAnnotations()">Exportera</button>
|
|
</div>
|
|
|
|
<div class="canvas-area">
|
|
<canvas id="annotationCanvas"></canvas>
|
|
|
|
<div class="ukg-context">
|
|
<h3>UKG Kontext</h3>
|
|
<div id="ukg-items">
|
|
<div class="ukg-item">Inga tidigare observationer</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hierarchy" id="hierarchy">
|
|
<div class="hierarchy-item level-1" onclick="selectType('infrastructure')">Infrastructure</div>
|
|
<div class="hierarchy-item level-2" onclick="selectType('road')">Road</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('asphalt')">Asphalt</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('crack')">Crack</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('pothole')">Pothole</div>
|
|
<div class="hierarchy-item level-2" onclick="selectType('lighting')">Lighting</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('street_lamp')">Street Lamp</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('traffic_light')">Traffic Light</div>
|
|
<div class="hierarchy-item level-2" onclick="selectType('utility')">Utility</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('electrical_cabinet')">Electrical Cabinet</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('manhole')">Manhole</div>
|
|
<div class="hierarchy-item level-2" onclick="selectType('signage')">Signage</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('stop')">Stop</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('speed')">Speed</div>
|
|
<div class="hierarchy-item level-2" onclick="selectType('vegetation')">Vegetation</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('tree')">Tree</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('bush')">Bush</div>
|
|
<div class="hierarchy-item level-2" onclick="selectType('furniture')">Furniture</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('bench')">Bench</div>
|
|
<div class="hierarchy-item level-3" onclick="selectType('trash_can')">Trash Can</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="navigation">
|
|
<button class="nav-btn" onclick="prevImage()">Föregående</button>
|
|
<span class="progress" id="progress">Bild 1 av 10</span>
|
|
<button class="nav-btn primary" onclick="nextImage()">Nästa</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// State
|
|
let currentTool = 'select';
|
|
let currentImage = 0;
|
|
let images = [];
|
|
let proposals = [];
|
|
let annotations = [];
|
|
let selectedProposal = null;
|
|
|
|
// Mock data
|
|
const mockProposals = [
|
|
{ id: 'p1', type: 'street_lamp', typeLabel: 'Gatlykta', confidence: 0.85, bbox: [100, 150, 200, 400], models: ['yolo', 'groundingDINO'], status: 'pending' },
|
|
{ id: 'p2', type: 'traffic_sign', typeLabel: 'Trafikskylt', confidence: 0.72, bbox: [300, 200, 380, 320], models: ['groundingDINO'], status: 'pending' },
|
|
{ id: 'p3', type: 'tree', typeLabel: 'Träd', confidence: 0.91, bbox: [500, 100, 600, 450], models: ['yolo', 'sam2'], status: 'pending' },
|
|
{ id: 'p4', type: 'manhole', typeLabel: 'Brunn', confidence: 0.45, bbox: [250, 380, 300, 420], models: ['groundingDINO'], status: 'pending' },
|
|
];
|
|
|
|
// Init
|
|
function init() {
|
|
loadProposals(mockProposals);
|
|
updateStats();
|
|
}
|
|
|
|
function loadProposals(newProposals) {
|
|
proposals = newProposals;
|
|
const container = document.getElementById('proposals');
|
|
container.innerHTML = '';
|
|
|
|
proposals.forEach((prop, index) => {
|
|
const div = document.createElement('div');
|
|
div.className = 'proposal';
|
|
div.dataset.index = index;
|
|
div.onclick = () => selectProposal(index);
|
|
|
|
const confidenceClass = prop.confidence > 0.8 ? 'confidence-high' : prop.confidence > 0.6 ? 'confidence-medium' : 'confidence-low';
|
|
|
|
div.innerHTML = `
|
|
<div class="proposal-header">
|
|
<span class="proposal-type">${prop.typeLabel}</span>
|
|
<span class="proposal-confidence ${confidenceClass}">${(prop.confidence * 100).toFixed(0)}%</span>
|
|
</div>
|
|
<div class="proposal-meta">${prop.models.length} modeller håller med</div>
|
|
<div class="proposal-models">
|
|
${prop.models.map(m => `<span class="model-tag">${m}</span>`).join('')}
|
|
</div>
|
|
<div class="proposal-actions">
|
|
<button class="action-btn btn-approve" onclick="approveProposal(${index}, event)">Godkänn</button>
|
|
<button class="action-btn btn-reject" onclick="rejectProposal(${index}, event)">Avvisa</button>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function selectProposal(index) {
|
|
document.querySelectorAll('.proposal').forEach(p => p.classList.remove('selected'));
|
|
document.querySelector(`.proposal[data-index="${index}"]`).classList.add('selected');
|
|
selectedProposal = proposals[index];
|
|
}
|
|
|
|
function approveProposal(index, event) {
|
|
event.stopPropagation();
|
|
proposals[index].status = 'approved';
|
|
updateStats();
|
|
renderProposals();
|
|
}
|
|
|
|
function rejectProposal(index, event) {
|
|
event.stopPropagation();
|
|
proposals[index].status = 'rejected';
|
|
updateStats();
|
|
renderProposals();
|
|
}
|
|
|
|
function updateStats() {
|
|
document.getElementById('stat-total').textContent = proposals.length;
|
|
document.getElementById('stat-approved').textContent = proposals.filter(p => p.status === 'approved').length;
|
|
document.getElementById('stat-rejected').textContent = proposals.filter(p => p.status === 'rejected').length;
|
|
document.getElementById('stat-corrected').textContent = proposals.filter(p => p.status === 'corrected').length;
|
|
}
|
|
|
|
function renderProposals() {
|
|
// Uppdatera UI baserat på status
|
|
proposals.forEach((prop, index) => {
|
|
const el = document.querySelector(`.proposal[data-index="${index}"]`);
|
|
if (prop.status === 'approved') {
|
|
el.style.opacity = '0.7';
|
|
el.style.borderLeft = '4px solid #4ade80';
|
|
} else if (prop.status === 'rejected') {
|
|
el.style.opacity = '0.4';
|
|
el.style.textDecoration = 'line-through';
|
|
}
|
|
});
|
|
}
|
|
|
|
function setTool(tool) {
|
|
currentTool = tool;
|
|
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
}
|
|
|
|
function toggleHierarchy() {
|
|
document.getElementById('hierarchy').classList.toggle('show');
|
|
}
|
|
|
|
function selectType(type) {
|
|
console.log('Selected type:', type);
|
|
document.getElementById('hierarchy').classList.remove('show');
|
|
}
|
|
|
|
function loadAIProposals() {
|
|
// I verkligheten: hämta från weak supervision API
|
|
console.log('Loading AI proposals...');
|
|
}
|
|
|
|
function exportAnnotations() {
|
|
const approved = proposals.filter(p => p.status === 'approved');
|
|
const data = {
|
|
version: '1.0',
|
|
generatedAt: new Date().toISOString(),
|
|
annotations: approved,
|
|
};
|
|
console.log('Exporting:', data);
|
|
alert(`Exporterade ${approved.length} annotationer`);
|
|
}
|
|
|
|
function prevImage() {
|
|
if (currentImage > 0) {
|
|
currentImage--;
|
|
updateProgress();
|
|
}
|
|
}
|
|
|
|
function nextImage() {
|
|
currentImage++;
|
|
updateProgress();
|
|
}
|
|
|
|
function updateProgress() {
|
|
document.getElementById('progress').textContent = `Bild ${currentImage + 1} av ${images.length || 10}`;
|
|
}
|
|
|
|
// Start
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|