import { MODEL_OPTIONS, classifyImage, getModelOption } from './classifier.js'; const imageInput = document.querySelector('#image-input'); const modelSelect = document.querySelector('#model-select'); const modelNote = document.querySelector('#model-note'); const clipControls = document.querySelector('#clip-controls'); const candidateLabels = document.querySelector('#candidate-labels'); const topK = document.querySelector('#top-k'); const topKValue = document.querySelector('#top-k-value'); const runButton = document.querySelector('#run-button'); const statusEl = document.querySelector('#status'); const progressWrap = document.querySelector('#progress-wrap'); const progressBar = document.querySelector('#progress-bar'); const previewWrap = document.querySelector('#preview-wrap'); const previewImage = document.querySelector('#preview-image'); const summary = document.querySelector('#summary'); const tbody = document.querySelector('#predictions-body'); const runtimeBadge = document.querySelector('#runtime-badge'); const sampleGrid = document.querySelector('#sample-grid'); const shuffleButton = document.querySelector('#shuffle-examples'); const runMeta = document.querySelector('#run-meta'); const metaModel = document.querySelector('#meta-model'); const metaImage = document.querySelector('#meta-image'); const metaRuntime = document.querySelector('#meta-runtime'); const historyBody = document.querySelector('#history-body'); const clearHistory = document.querySelector('#clear-history'); const state = { dataUrl: null, sourceLabel: null, running: false, history: [] }; const EXAMPLES = [ { src: './examples/chelsea.png', label: 'Cat' }, { src: './examples/coffee.png', label: 'Coffee' }, { src: './examples/rocket.jpg', label: 'Rocket' }, { src: './examples/astronaut.png', label: 'Astronaut' }, { src: './examples/cheetah.jpg', label: 'Cheetah' }, { src: './examples/lion.jpg', label: 'Lion' }, { src: './examples/tower.jpg', label: 'Tower' }, { src: './examples/bus.png', label: 'Bus illustration' }, { src: './examples/dog.jpg', label: 'Dog' }, { src: './examples/horse.jpg', label: 'Horse' }, { src: './examples/elephant.jpg', label: 'Elephant' }, { src: './examples/rabbit.jpg', label: 'Rabbit' }, { src: './examples/bird.jpg', label: 'Parrot' }, { src: './examples/cow.jpg', label: 'Cow' }, { src: './examples/airplane.jpg', label: 'Airplane' }, { src: './examples/car.jpg', label: 'Car' }, { src: './examples/bicycle.jpg', label: 'Bicycle' }, { src: './examples/train.jpg', label: 'Train' }, { src: './examples/motorcycle.jpg', label: 'Motorcycle' }, { src: './examples/boat.jpg', label: 'Boat' }, { src: './examples/truck.jpg', label: 'Truck' }, { src: './examples/pizza.jpg', label: 'Pizza' }, { src: './examples/hamburger.jpg', label: 'Hamburger' }, { src: './examples/apple.jpg', label: 'Apple' }, { src: './examples/banana.jpg', label: 'Banana' }, { src: './examples/orange.jpg', label: 'Orange' }, { src: './examples/sandwich.jpg', label: 'Sandwich' }, { src: './examples/bread.jpg', label: 'Bread' }, { src: './examples/strawberry.jpg', label: 'Strawberry' }, { src: './examples/ice_cream.jpg', label: 'Ice cream' }, { src: './examples/tiger.jpg', label: 'Tiger' }, { src: './examples/zebra.jpg', label: 'Zebra' }, { src: './examples/giraffe.jpg', label: 'Giraffe' }, { src: './examples/panda.jpg', label: 'Giant panda' }, { src: './examples/koala.jpg', label: 'Koala' }, { src: './examples/kangaroo.jpg', label: 'Kangaroo' }, { src: './examples/camel.jpg', label: 'Camel' }, { src: './examples/goat.jpg', label: 'Goat' }, { src: './examples/sheep.jpg', label: 'Sheep' }, { src: './examples/pig.jpg', label: 'Pig' }, { src: './examples/duck.jpg', label: 'Duck' }, { src: './examples/chicken.jpg', label: 'Chicken' }, { src: './examples/flamingo.jpg', label: 'Flamingo' }, { src: './examples/penguin.jpg', label: 'Penguin' }, { src: './examples/owl.jpg', label: 'Owl' }, { src: './examples/eagle.jpg', label: 'Eagle' }, { src: './examples/butterfly.jpg', label: 'Butterfly' }, { src: './examples/bee.jpg', label: 'Bee' }, { src: './examples/turtle.jpg', label: 'Turtle' }, { src: './examples/frog.jpg', label: 'Frog' }, { src: './examples/city_bus.jpg', label: 'City bus' }, { src: './examples/taxi.jpg', label: 'Taxi' }, { src: './examples/tram.jpg', label: 'Tram' }, { src: './examples/subway.jpg', label: 'Subway train' }, { src: './examples/helicopter.jpg', label: 'Helicopter' }, { src: './examples/sailboat.jpg', label: 'Sailboat' }, { src: './examples/ship.jpg', label: 'Ship' }, { src: './examples/canoe.jpg', label: 'Canoe' }, { src: './examples/tractor.jpg', label: 'Tractor' }, { src: './examples/fire_truck.jpg', label: 'Fire truck' }, { src: './examples/ambulance.jpg', label: 'Ambulance' }, { src: './examples/police_car.jpg', label: 'Police car' }, { src: './examples/van.jpg', label: 'Van' }, { src: './examples/scooter.jpg', label: 'Scooter' }, { src: './examples/hot_air_balloon.jpg', label: 'Hot-air balloon' }, { src: './examples/excavator.jpg', label: 'Excavator' }, { src: './examples/bulldozer.jpg', label: 'Bulldozer' }, { src: './examples/forklift.jpg', label: 'Forklift' }, { src: './examples/jet.jpg', label: 'Jet aircraft' }, { src: './examples/cable_car.jpg', label: 'Cable car' }, { src: './examples/pear.jpg', label: 'Pear' }, { src: './examples/peach.jpg', label: 'Peach' }, { src: './examples/pineapple.jpg', label: 'Pineapple' }, { src: './examples/watermelon.jpg', label: 'Watermelon' }, { src: './examples/grapes.jpg', label: 'Grapes' }, { src: './examples/mango.jpg', label: 'Mango' }, { src: './examples/lemon.jpg', label: 'Lemon' }, { src: './examples/avocado.jpg', label: 'Avocado' }, { src: './examples/carrot.jpg', label: 'Carrot' }, { src: './examples/broccoli.jpg', label: 'Broccoli' }, { src: './examples/tomato.jpg', label: 'Tomato' }, { src: './examples/corn.jpg', label: 'Corn' }, { src: './examples/donut.jpg', label: 'Doughnut' }, { src: './examples/cookie.jpg', label: 'Cookie' }, { src: './examples/cupcake.jpg', label: 'Cupcake' }, { src: './examples/sushi.jpg', label: 'Sushi' }, { src: './examples/pasta.jpg', label: 'Pasta' }, { src: './examples/salad.jpg', label: 'Salad' }, { src: './examples/popcorn.jpg', label: 'Popcorn' }, { src: './examples/egg.jpg', label: 'Egg' }, { src: './examples/laptop.jpg', label: 'Laptop' }, { src: './examples/camera.jpg', label: 'Camera' }, { src: './examples/guitar.jpg', label: 'Guitar' }, { src: './examples/piano.jpg', label: 'Piano' }, { src: './examples/clock.jpg', label: 'Clock' }, { src: './examples/chair.jpg', label: 'Chair' }, { src: './examples/lamp.jpg', label: 'Lamp' }, { src: './examples/backpack.jpg', label: 'Backpack' }, { src: './examples/umbrella.jpg', label: 'Umbrella' }, { src: './examples/book.jpg', label: 'Book' }, ]; function chooseRandom(items, count) { return [...items] .map((item) => ({ item, sort: Math.random() })) .sort((a, b) => a.sort - b.sort) .slice(0, count) .map(({ item }) => item); } function resetResults() { summary.textContent = 'No predictions yet.'; tbody.innerHTML = 'Run the classifier to see details.'; runMeta.hidden = true; } function setBadge(text, mode = '') { runtimeBadge.textContent = text; runtimeBadge.className = `badge ${mode}`.trim(); } function setRunning(running) { state.running = running; runButton.disabled = running || !state.dataUrl; modelSelect.disabled = running; imageInput.disabled = running; topK.disabled = running; candidateLabels.disabled = running; shuffleButton.disabled = running; document.querySelectorAll('.sample-card').forEach((card) => { card.disabled = running; }); } function setSelectedImage(dataUrl, label, readyMessage) { const image = new Image(); image.onload = () => { state.dataUrl = dataUrl; state.sourceLabel = label; previewImage.src = dataUrl; previewImage.alt = `${label} selected for classification`; previewWrap.classList.remove('empty'); setRunning(false); statusEl.textContent = readyMessage; setBadge('Image ready'); resetResults(); }; image.onerror = () => { statusEl.textContent = 'Could not read that image.'; setBadge('Image error', 'error'); }; image.src = dataUrl; } function blobToDataUrl(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } async function loadExample(example, button) { try { statusEl.textContent = `Loading example: ${example.label}…`; setBadge('Loading sample', 'running'); const response = await fetch(example.src); if (!response.ok) throw new Error(`Could not load ${example.src}`); const dataUrl = await blobToDataUrl(await response.blob()); imageInput.value = ''; document.querySelectorAll('.sample-card').forEach((card) => card.classList.remove('selected')); button.classList.add('selected'); setSelectedImage(dataUrl, example.label, `${example.label} example ready. Click Run AI Classification.`); } catch (error) { console.error(error); statusEl.textContent = 'Could not load that example. Try another image.'; setBadge('Sample error', 'error'); } } function renderModelChoices() { for (const option of MODEL_OPTIONS) { const item = document.createElement('option'); item.value = option.key; item.textContent = option.label; modelSelect.appendChild(item); } modelSelect.value = MODEL_OPTIONS[0].key; modelNote.textContent = MODEL_OPTIONS[0].note; } function renderExampleChoices() { sampleGrid.innerHTML = ''; for (const example of chooseRandom(EXAMPLES, 4)) { const button = document.createElement('button'); button.type = 'button'; button.className = 'sample-card'; button.title = `Use sample: ${example.label}`; const image = document.createElement('img'); image.src = example.src; image.alt = example.label; image.loading = 'lazy'; const label = document.createElement('span'); label.textContent = example.label; const source = document.createElement('small'); source.textContent = 'Tested teaching sample'; button.append(image, label, source); button.addEventListener('click', () => loadExample(example, button)); sampleGrid.appendChild(button); } } function setProgress(data) { let percent = null; if (typeof data?.progress === 'number') percent = data.progress; else if (data?.total && typeof data?.loaded === 'number') percent = (data.loaded / data.total) * 100; if (percent !== null && Number.isFinite(percent)) { progressWrap.hidden = false; progressBar.style.width = `${Math.max(0, Math.min(100, percent))}%`; } if (data?.status === 'progress' && data?.file) { statusEl.textContent = `Downloading model: ${data.file}`; } } function parseCandidateLabels() { return candidateLabels.value.split(/[,\n]/).map((label) => label.trim()).filter(Boolean); } function renderPredictions(predictions) { tbody.innerHTML = ''; const option = getModelOption(modelSelect.value); const scoreContext = option.task === 'zero-shot-image-classification' ? 'Scores compare only the candidate labels supplied above.' : 'Scores are calculated over the 1,000 ImageNet classes.'; predictions.forEach((prediction, index) => { const row = document.createElement('tr'); const rankCell = document.createElement('td'); rankCell.textContent = index + 1; const labelCell = document.createElement('td'); labelCell.textContent = prediction.label; const score = Math.max(0, Math.min(1, Number(prediction.score))); const scoreCell = document.createElement('td'); const scoreWrap = document.createElement('div'); scoreWrap.className = 'score-wrap'; const scoreText = document.createElement('span'); scoreText.textContent = `${(score * 100).toFixed(2)}%`; const scoreTrack = document.createElement('span'); scoreTrack.className = 'score-track'; const scoreFill = document.createElement('span'); scoreFill.className = 'score-fill'; scoreFill.style.width = `${score * 100}%`; scoreTrack.appendChild(scoreFill); scoreWrap.append(scoreText, scoreTrack); scoreCell.appendChild(scoreWrap); row.append(rankCell, labelCell, scoreCell); tbody.appendChild(row); }); const best = predictions[0]; summary.textContent = best ? `${option.label} predicts “${best.label}” at ${(Number(best.score) * 100).toFixed(1)}%. ${scoreContext}` : `${option.label} returned no predictions.`; } function renderHistory() { clearHistory.disabled = state.history.length === 0; if (!state.history.length) { historyBody.innerHTML = 'Run two models on the same image to start a comparison.'; return; } historyBody.innerHTML = ''; state.history.forEach((item, index) => { const row = document.createElement('tr'); const values = [ index + 1, item.image, item.model, item.label, `${(item.score * 100).toFixed(1)}%`, `${item.seconds.toFixed(1)}s`, ]; values.forEach((value) => { const cell = document.createElement('td'); cell.textContent = value; row.appendChild(cell); }); historyBody.appendChild(row); }); } function recordRun(option, predictions, seconds) { const best = predictions[0]; if (!best) return; state.history.unshift({ image: state.sourceLabel || 'Selected image', model: option.label, label: best.label, score: Number(best.score), seconds, }); state.history = state.history.slice(0, 12); renderHistory(); metaModel.textContent = option.label; metaImage.textContent = state.sourceLabel || 'Selected image'; metaRuntime.textContent = `${seconds.toFixed(1)}s`; runMeta.hidden = false; } topK.addEventListener('input', () => { topKValue.value = topK.value; }); modelSelect.addEventListener('change', () => { const option = getModelOption(modelSelect.value); modelNote.textContent = option.note; clipControls.hidden = option.task !== 'zero-shot-image-classification'; setBadge(`Model: ${option.label}`); resetResults(); if (state.dataUrl) statusEl.textContent = `Model changed to ${option.label}. Run again to compare predictions.`; }); imageInput.addEventListener('change', () => { const file = imageInput.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { statusEl.textContent = 'Please choose an image file.'; setBadge('Invalid file', 'error'); return; } if (file.size > 12 * 1024 * 1024) { statusEl.textContent = 'Please choose an image smaller than 12 MB.'; setBadge('Image too large', 'error'); return; } document.querySelectorAll('.sample-card').forEach((card) => card.classList.remove('selected')); const reader = new FileReader(); reader.onload = () => setSelectedImage(reader.result, 'Uploaded image', 'Your image is ready. Click Run AI Classification.'); reader.onerror = () => { statusEl.textContent = 'Could not read that image.'; setBadge('Image error', 'error'); }; reader.readAsDataURL(file); }); clearHistory.addEventListener('click', () => { state.history = []; renderHistory(); }); shuffleButton.addEventListener('click', () => { renderExampleChoices(); statusEl.textContent = state.dataUrl ? 'New examples shown. Your current image is still selected.' : 'Choose an example, or upload your own image.'; }); runButton.addEventListener('click', async () => { if (!state.dataUrl || state.running) return; const option = getModelOption(modelSelect.value); const labels = parseCandidateLabels(); if (option.task === 'zero-shot-image-classification' && (labels.length < 2 || labels.length > 20)) { statusEl.textContent = 'CLIP needs 2–20 candidate labels.'; setBadge('Check labels', 'error'); return; } setRunning(true); setBadge(`Loading ${option.label}`, 'running'); progressWrap.hidden = false; progressBar.style.width = '4%'; const started = performance.now(); try { statusEl.textContent = `Preparing ${option.label}. The first run may download model files.`; const predictions = await classifyImage(state.dataUrl, { modelKey: option.key, topK: Number(topK.value), candidateLabels: labels, progressCallback: setProgress, }); renderPredictions(predictions); const seconds = (performance.now() - started) / 1000; recordRun(option, predictions, seconds); statusEl.textContent = `Done in ${seconds.toFixed(1)}s with ${option.label}. Change the model or image to compare.`; setBadge(`${predictions.length} predictions`, 'ready'); } catch (error) { console.error(error); statusEl.textContent = `${option.label} failed to run. Check the network connection or try ResNet-18.`; setBadge('Error', 'error'); } finally { progressWrap.hidden = true; setRunning(false); } }); renderModelChoices(); renderExampleChoices(); renderHistory();