Spaces:
Running
Running
| {% extends 'base.html' %} | |
| {% load static %} | |
| {% block title %}Results — Constellar{% endblock %} | |
| {% block extra_css %} | |
| <style> | |
| .result-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 1.375rem; | |
| transition: border-color .2s; | |
| } | |
| .result-card:hover { border-color: rgba(99,102,241,.3); } | |
| .confidence-badge { | |
| font-size: .6875rem; font-weight: 700; | |
| padding: .25rem .625rem; | |
| border-radius: 5px; | |
| background: rgba(34,197,94,.12); | |
| border: 1px solid rgba(34,197,94,.25); | |
| color: #86efac; | |
| } | |
| .icon-action { | |
| width: 30px; height: 30px; | |
| border-radius: 7px; | |
| border: 1px solid var(--border); | |
| background: rgba(255,255,255,.04); | |
| color: var(--muted); | |
| cursor: pointer; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: all .15s; | |
| } | |
| .icon-action:hover { color: #fff; border-color: rgba(255,255,255,.18); } | |
| .learn-btn { | |
| font-family: 'Inter', sans-serif; | |
| font-size: .78125rem; font-weight: 600; | |
| color: #a5b4fc; | |
| padding: .5rem 1rem; | |
| border-radius: 7px; | |
| border: 1px solid rgba(99,102,241,.3); | |
| background: rgba(99,102,241,.08); | |
| cursor: pointer; | |
| transition: all .15s; | |
| display: inline-flex; align-items: center; gap: .375rem; | |
| } | |
| .learn-btn:hover { background: rgba(99,102,241,.16); border-color: rgba(99,102,241,.5); } | |
| .coords-box { | |
| margin-top: 1rem; | |
| padding: .625rem .875rem; | |
| background: rgba(255,255,255,.025); | |
| border: 1px solid var(--border); | |
| border-radius: 7px; | |
| font-size: .6875rem; | |
| color: var(--muted); | |
| display: flex; gap: 1.25rem; | |
| } | |
| .spin { | |
| width: 14px; height: 14px; border-radius: 50%; | |
| border: 2px solid rgba(255,255,255,.15); | |
| border-top-color: var(--accent); | |
| animation: spin .7s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(8px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .animate-fade-in { animation: fadeIn .3s ease-out; } | |
| /* ── before/after slider ── */ | |
| .compare { | |
| position: relative; | |
| overflow: hidden; | |
| border-radius: 9px; | |
| user-select: none; | |
| } | |
| .compare > img { width: 100%; display: block; } | |
| .compare-top { | |
| position: absolute; | |
| top: 0; left: 0; | |
| height: 100%; | |
| width: var(--pos, 50%); | |
| overflow: hidden; | |
| } | |
| .compare-top img { | |
| position: absolute; | |
| top: 0; left: 0; | |
| height: 100%; | |
| width: auto; | |
| max-width: none; | |
| } | |
| .compare-line { | |
| position: absolute; | |
| top: 0; bottom: 0; | |
| left: var(--pos, 50%); | |
| width: 2px; | |
| background: rgba(255,255,255,.85); | |
| box-shadow: 0 0 10px rgba(0,0,0,.5); | |
| pointer-events: none; | |
| } | |
| .compare-line::after { | |
| content: '⇄'; | |
| position: absolute; | |
| top: 50%; left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 32px; height: 32px; | |
| border-radius: 50%; | |
| background: rgba(6,13,31,.85); | |
| border: 1px solid rgba(255,255,255,.5); | |
| color: #fff; | |
| font-size: .875rem; | |
| display: flex; align-items: center; justify-content: center; | |
| } | |
| .compare-label { | |
| position: absolute; | |
| top: .75rem; | |
| font-size: .625rem; font-weight: 700; | |
| letter-spacing: .08em; text-transform: uppercase; | |
| padding: .25rem .5rem; | |
| border-radius: 5px; | |
| background: rgba(6,13,31,.75); | |
| color: rgba(255,255,255,.85); | |
| pointer-events: none; | |
| backdrop-filter: blur(4px); | |
| } | |
| .compare input[type="range"] { | |
| position: absolute; | |
| inset: 0; | |
| width: 100%; height: 100%; | |
| opacity: 0; | |
| cursor: ew-resize; | |
| margin: 0; | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <!-- ════ HEADER ════ --> | |
| <section class="section" style="padding-top:3.5rem; padding-bottom:2rem; text-align:center;"> | |
| <div class="tag" style="margin-bottom:1.25rem;">Analysis Complete</div> | |
| <h1 style="font-family:'Orbitron',monospace; font-size:clamp(1.5rem,3.5vw,2.25rem); font-weight:900; margin:0 0 .875rem; letter-spacing:-.01em;"> | |
| Detection Results | |
| </h1> | |
| <p style="font-size:.9375rem; color:var(--muted); margin:0;"> | |
| {% if analysis_type == 'constellation' %} | |
| Your night sky image has been analyzed for constellation patterns. | |
| {% else %} | |
| Objects detected in your image. | |
| {% endif %} | |
| </p> | |
| </section> | |
| <!-- ════ PROCESSED IMAGE ════ --> | |
| <section style="padding:0 1.5rem;"> | |
| <div style="max-width:900px; margin:0 auto;"> | |
| <div class="glass-card" style="padding:1.25rem;"> | |
| {% if original_image %} | |
| <!-- before/after comparison slider --> | |
| <div class="compare" id="compareBox" style="--pos:50%;"> | |
| <img src="data:image/jpeg;base64,{{ processed_image }}" alt="Detection result" draggable="false"> | |
| <div class="compare-top"> | |
| <img src="data:image/jpeg;base64,{{ original_image }}" alt="Original image" draggable="false"> | |
| </div> | |
| <div class="compare-line"></div> | |
| <span class="compare-label" style="left:.75rem;">Original</span> | |
| <span class="compare-label" style="right:.75rem;">Detected</span> | |
| <input type="range" min="0" max="100" value="50" id="compareRange" aria-label="Compare original and detected image"> | |
| </div> | |
| <p style="text-align:center; font-size:.71875rem; color:var(--muted); margin:.875rem 0 0;"> | |
| Drag the slider to compare your original photo with the detection result | |
| </p> | |
| {% else %} | |
| <img src="data:image/jpeg;base64,{{ processed_image }}" | |
| alt="Processed constellation image" | |
| style="width:100%; border-radius:9px; display:block;"> | |
| {% endif %} | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ════ DETECTIONS ════ --> | |
| <section class="section" style="max-width:900px; padding-top:2.5rem; padding-bottom:2rem;"> | |
| {% if detected_constellations %} | |
| <p class="section-label">Detected</p> | |
| <h2 style="font-family:'Orbitron',monospace; font-size:clamp(1.2rem,2.5vw,1.5rem); font-weight:700; margin:0 0 1.5rem;"> | |
| {{ detected_constellations|length }} Constellation{{ detected_constellations|length|pluralize }} Found | |
| </h2> | |
| <div style="display:grid; grid-template-columns:repeat(auto-fill,minmax(270px,1fr)); gap:1rem;"> | |
| {% for constellation in detected_constellations %} | |
| <div class="result-card"> | |
| <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1rem;"> | |
| <h3 style="font-size:1rem; font-weight:600; margin:0; color:#fff;">{{ constellation.name }}</h3> | |
| <div style="display:flex; align-items:center; gap:.5rem;"> | |
| <span class="confidence-badge">{{ constellation.confidence|floatformat:2 }}</span> | |
| <button onclick="speakConstellation('{{ constellation.name }}')" class="icon-action" title="Listen"> | |
| <svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> | |
| <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/> | |
| <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="info-{{ forloop.counter }}" class="constellation-info"> | |
| <button onclick="loadConstellationInfo('{{ constellation.name }}', {{ forloop.counter }})" class="learn-btn"> | |
| <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> | |
| <circle cx="12" cy="12" r="10"/><path stroke-linecap="round" d="M12 16v-4M12 8h.01"/> | |
| </svg> | |
| Learn more | |
| </button> | |
| </div> | |
| <div class="coords-box"> | |
| <span>X {{ constellation.coordinates.0|floatformat:0 }}–{{ constellation.coordinates.2|floatformat:0 }}</span> | |
| <span>Y {{ constellation.coordinates.1|floatformat:0 }}–{{ constellation.coordinates.3|floatformat:0 }}</span> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| {% else %} | |
| <div style="text-align:center; padding:3rem 1rem;"> | |
| <div style="width:56px; height:56px; border-radius:14px; background:rgba(255,255,255,.04); border:1px solid var(--border); display:flex; align-items:center; justify-content:center; margin:0 auto 1.25rem;"> | |
| <svg width="24" height="24" fill="none" stroke="rgba(221,228,240,.4)" stroke-width="1.6" viewBox="0 0 24 24"> | |
| <circle cx="11" cy="11" r="8"/><path stroke-linecap="round" d="m21 21-4.35-4.35M8 11h6"/> | |
| </svg> | |
| </div> | |
| <h3 style="font-size:1rem; font-weight:600; margin:0 0 .5rem;">No constellations detected</h3> | |
| <p style="font-size:.8125rem; color:var(--muted); margin:0;">Try a clearer night sky image with visible star patterns.</p> | |
| </div> | |
| {% endif %} | |
| </section> | |
| <!-- ════ NEXT ACTIONS ════ --> | |
| <section class="section" style="padding-top:1rem; text-align:center; border-top:1px solid var(--border);"> | |
| <p style="font-size:.8125rem; color:var(--muted); margin:2rem 0 1rem;">What's next?</p> | |
| <div style="display:flex; gap:.75rem; justify-content:center; flex-wrap:wrap;"> | |
| <a href="{% url 'upload' %}" class="btn-accent" style="padding:.65rem 1.375rem;">Analyze Another Image</a> | |
| <a href="{% url 'detect' %}" class="btn-ghost" style="padding:.65rem 1.375rem;">Live Detection</a> | |
| <a href="{% url 'database' %}" class="btn-ghost" style="padding:.65rem 1.375rem;">Browse Database</a> | |
| </div> | |
| </section> | |
| {% endblock %} | |
| {% block extra_js %} | |
| <script> | |
| function speakConstellation(constellationName) { | |
| if ('speechSynthesis' in window) { | |
| speechSynthesis.cancel(); | |
| const infoElement = document.querySelector(`[data-constellation="${constellationName}"]`); | |
| let textToSpeak = `Constellation: ${constellationName}`; | |
| if (infoElement) { | |
| const description = infoElement.querySelector('.constellation-description'); | |
| if (description) textToSpeak += `. ${description.textContent}`; | |
| } | |
| const utterance = new SpeechSynthesisUtterance(textToSpeak); | |
| utterance.rate = 0.8; | |
| utterance.pitch = 1; | |
| utterance.volume = 0.8; | |
| speechSynthesis.speak(utterance); | |
| } else { | |
| alert('Text-to-speech is not supported in your browser'); | |
| } | |
| } | |
| async function loadConstellationInfo(constellationName, index) { | |
| const infoDiv = document.getElementById(`info-${index}`); | |
| infoDiv.innerHTML = ` | |
| <div style="display:flex; align-items:center; gap:.5rem; padding:.375rem 0;"> | |
| <div class="spin"></div> | |
| <span style="font-size:.78125rem; color:var(--muted);">Loading details…</span> | |
| </div> | |
| `; | |
| try { | |
| const response = await fetch('/get-constellation-info/', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-CSRFToken': getCookie('csrftoken'), | |
| }, | |
| body: JSON.stringify({ constellation_name: constellationName }) | |
| }); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| displayConstellationInfo(infoDiv, data.name, data.info); | |
| } else { | |
| throw new Error('Failed to fetch constellation info'); | |
| } | |
| } catch (error) { | |
| displayConstellationInfo(infoDiv, constellationName, getBasicConstellationInfo(constellationName)); | |
| } | |
| } | |
| function displayConstellationInfo(container, name, info) { | |
| container.innerHTML = ` | |
| <div data-constellation="${name}" class="animate-fade-in"> | |
| <div class="constellation-description" style="font-size:.78125rem; color:var(--muted); line-height:1.7; margin-bottom:.75rem;"> | |
| ${info} | |
| </div> | |
| <button onclick="speakConstellation('${name}')" class="learn-btn"> | |
| <svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> | |
| <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/> | |
| <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/> | |
| </svg> | |
| Listen | |
| </button> | |
| </div> | |
| `; | |
| } | |
| function getBasicConstellationInfo(name) { | |
| const basicInfo = { | |
| 'Orion': 'Orion is one of the most recognizable constellations, known as "The Hunter." It contains bright stars like Betelgeuse and Rigel, and the famous Orion Nebula.', | |
| 'Ursa Major': 'Ursa Major, the Great Bear, contains the famous Big Dipper asterism. It\'s visible year-round in most northern hemisphere locations.', | |
| 'Cassiopeia': 'Cassiopeia is known for its distinctive W-shape formed by five bright stars. In Greek mythology, she was a vain queen.', | |
| 'Leo': 'Leo the Lion is a zodiac constellation best seen in spring. Its brightest star is Regulus, marking the lion\'s heart.', | |
| 'Cygnus': 'Cygnus, the Swan, flies along the Milky Way. Its brightest star Deneb forms part of the Summer Triangle.', | |
| }; | |
| return basicInfo[name] || `${name} is a fascinating constellation with a rich history in astronomy and mythology. This celestial pattern has guided navigators and inspired storytellers for thousands of years.`; | |
| } | |
| function getCookie(name) { | |
| let cookieValue = null; | |
| if (document.cookie && document.cookie !== '') { | |
| const cookies = document.cookie.split(';'); | |
| for (let i = 0; i < cookies.length; i++) { | |
| const cookie = cookies[i].trim(); | |
| if (cookie.substring(0, name.length + 1) === (name + '=')) { | |
| cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); | |
| break; | |
| } | |
| } | |
| } | |
| return cookieValue; | |
| } | |
| const compareBox = document.getElementById('compareBox'); | |
| const compareRange = document.getElementById('compareRange'); | |
| if (compareBox && compareRange) { | |
| compareRange.addEventListener('input', () => { | |
| compareBox.style.setProperty('--pos', compareRange.value + '%'); | |
| }); | |
| } | |
| </script> | |
| {% endblock %} | |