Spaces:
Sleeping
Sleeping
| // inject viewport meta if missing (fix iOS auto zoom & layout scale) | |
| (function () { | |
| if (!document.querySelector('meta[name="viewport"]')) { | |
| var m = document.createElement('meta'); | |
| m.name = 'viewport'; | |
| m.content = 'width=device-width, initial-scale=1, viewport-fit=cover'; | |
| document.head.appendChild(m); | |
| } | |
| })(); | |
| // Chinese (backend / DB) -> English display maps. | |
| // NOTE: <select> option VALUES stay Chinese so the /match backend keeps working; | |
| // these maps are only for human-readable display. | |
| const COLOR_EN = { | |
| "白色": "White", "透明": "Transparent", "黑色": "Black", "棕色": "Brown", | |
| "紅色": "Red", "橘色": "Orange", "皮膚色": "Beige", "黃色": "Yellow", | |
| "綠色": "Green", "藍色": "Blue", "紫色": "Purple", "粉紅色": "Pink", "灰色": "Gray" | |
| }; | |
| const COLOR_HEX = { | |
| "白色": "#ffffff", "透明": "#e5e7eb", "黑色": "#1f2937", "棕色": "#92400e", | |
| "紅色": "#dc2626", "橘色": "#ea580c", "皮膚色": "#e8c9a0", "黃色": "#facc15", | |
| "綠色": "#16a34a", "藍色": "#2563eb", "紫色": "#7c3aed", "粉紅色": "#ec4899", "灰色": "#9ca3af" | |
| }; | |
| const SHAPE_EN = { "圓形": "Round", "橢圓形": "Oval", "其他": "Other" }; | |
| // ===== DOM elements ===== | |
| const uploadInput = document.getElementById('uploadInput'); | |
| const customFileButton = document.getElementById('customFileButton'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const textField = document.getElementById('recognizedText'); | |
| const color1Select = document.getElementById('color1'); | |
| const color2Select = document.getElementById('color2'); | |
| const shapeSelect = document.getElementById('shape'); | |
| const confirmButton = document.getElementById('match-button'); | |
| const summaryBox = document.getElementById('detectionSummary'); | |
| const resultModal = document.getElementById('resultModal'); | |
| customFileButton.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', (event) => { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => Detection(e.target.result); | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| uploadInput.addEventListener('change', (event) => { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => Detection(e.target.result); | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| // Render the HSV color/shape detection summary | |
| function renderSummary(colors, shape) { | |
| if ((!colors || colors.length === 0) && !shape) { | |
| summaryBox.hidden = true; | |
| return; | |
| } | |
| const chips = []; | |
| (colors || []).forEach((c) => { | |
| if (!c) return; | |
| const hex = COLOR_HEX[c] || '#cccccc'; | |
| const name = COLOR_EN[c] || c; | |
| chips.push(`<span class="ds-chip"><span class="ds-swatch" style="background:${hex}"></span>${name}</span>`); | |
| }); | |
| if (shape) { | |
| chips.push(`<span class="ds-chip">⬭ ${SHAPE_EN[shape] || shape}</span>`); | |
| } | |
| summaryBox.innerHTML = | |
| `<div class="ds-title">Detected — HSV color & shape recognition</div> | |
| <div class="ds-chips">${chips.join('')}</div>`; | |
| summaryBox.hidden = false; | |
| } | |
| async function Detection(imageData) { | |
| try { | |
| const res = await fetch('/upload', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ image: imageData }) | |
| }); | |
| const ctype = res.headers.get('content-type') || ''; | |
| if (!res.ok || !ctype.includes('application/json')) { | |
| const text = await res.text(); | |
| console.warn('[UPLOAD] non-JSON response:', text); | |
| alert('🚨 Server error, please try again later.'); | |
| return; | |
| } | |
| const data = await res.json(); | |
| if (!data.ok) { | |
| alert('❌ Error: ' + (data.error || 'Unknown error')); | |
| return; | |
| } | |
| const result = data.result || {}; | |
| // Show cropped pill image | |
| const container = document.getElementById('photo-container'); | |
| container.innerHTML = ''; | |
| if (result.cropped_image) { | |
| const img = new Image(); | |
| img.src = result.cropped_image; | |
| img.alt = 'Cropped pill'; | |
| container.appendChild(img); | |
| } | |
| // Fill editable fields | |
| const colors = result['顏色'] || []; | |
| textField.value = (result['文字辨識'] || []).join(', '); | |
| color1Select.value = colors[0] || ''; | |
| color2Select.value = colors[1] || ''; | |
| shapeSelect.value = result['外型'] || ''; | |
| // Highlight the HSV detection result | |
| renderSummary(colors, result['外型']); | |
| } catch (err) { | |
| alert('🚨 Image recognition failed: ' + err.message); | |
| } | |
| } | |
| // Build a drug result card | |
| function buildCard(drug) { | |
| const card = document.createElement('div'); | |
| card.className = 'candidate-card'; | |
| card.innerHTML = ` | |
| ${drug.drug_image ? `<img src="${drug.drug_image}" alt="${drug.name || 'Medication'}">` : ''} | |
| <h3>${drug.name || 'Unknown'}</h3> | |
| <p><strong>Indications:</strong> ${drug.symptoms || 'N/A'}</p> | |
| <p><strong>Precautions:</strong> ${drug.precautions || 'N/A'}</p> | |
| <p><strong>Side effects:</strong> ${drug.side_effects || 'N/A'}</p> | |
| `; | |
| return card; | |
| } | |
| function showModal(cards) { | |
| const candidateList = document.getElementById('candidateList'); | |
| candidateList.innerHTML = ''; | |
| cards.forEach((c) => candidateList.appendChild(c)); | |
| resultModal.hidden = false; | |
| } | |
| confirmButton.addEventListener('click', async () => { | |
| const payload = { | |
| texts: textField.value.trim().split(',').map(t => t.trim()).filter(Boolean), | |
| colors: [color1Select.value.trim(), color2Select.value.trim()].filter(Boolean), | |
| shape: shapeSelect.value.trim() | |
| }; | |
| try { | |
| const response = await fetch('/match', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify(payload) | |
| }); | |
| const text = await response.text(); | |
| let result; | |
| try { | |
| result = JSON.parse(text); | |
| } catch (err) { | |
| alert('⚠️ JSON parse error: ' + err.message + '\nRaw response:\n' + text); | |
| return; | |
| } | |
| if (result.error) { | |
| alert('❌ Error: ' + result.error); | |
| return; | |
| } | |
| if (result.candidates) { | |
| showModal(result.candidates.map(buildCard)); | |
| } else if (result.name) { | |
| showModal([buildCard(result)]); | |
| } else { | |
| alert('❌ No matching results found.'); | |
| } | |
| } catch (error) { | |
| alert('🚨 Request failed: ' + error.message); | |
| } | |
| }); | |
| // Modal close handlers (attached once) | |
| document.getElementById('closeModal').addEventListener('click', () => { | |
| resultModal.hidden = true; | |
| }); | |
| resultModal.addEventListener('click', (e) => { | |
| if (e.target.id === 'resultModal') resultModal.hidden = true; | |
| }); | |