Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
199 lines
7.5 KiB
JavaScript
199 lines
7.5 KiB
JavaScript
const statusText = document.getElementById('status-text');
|
|
const wsIndicator = document.getElementById('ws-indicator');
|
|
const searchBtn = document.getElementById('search-btn');
|
|
const searchInput = document.getElementById('search-input');
|
|
const resultsContainer = document.getElementById('discovery-results');
|
|
|
|
const dropzone = document.getElementById('dropzone');
|
|
const fileInput = document.getElementById('file-input');
|
|
|
|
const dlBtn = document.getElementById('dl-btn');
|
|
const dlUrl = document.getElementById('dl-url');
|
|
const dlStart = document.getElementById('dl-start');
|
|
const dlEnd = document.getElementById('dl-end');
|
|
const confirmModal = document.getElementById('confirm-modal');
|
|
const dlConfirmBtn = document.getElementById('dl-confirm-btn');
|
|
const dlCancelBtn = document.getElementById('dl-cancel-btn');
|
|
|
|
|
|
// --- WebSocket ---
|
|
let ws;
|
|
function connectWS() {
|
|
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
const wsUrl = `${proto}://${window.location.host}/ws`;
|
|
// For local dev without docker, port might be 3000
|
|
const finalWsUrl = window.location.port === '' ? `${proto}://${window.location.hostname}:3000/ws` : wsUrl;
|
|
|
|
ws = new WebSocket(finalWsUrl);
|
|
|
|
ws.onopen = () => {
|
|
wsIndicator.className = 'h-2 w-2 rounded-full bg-green-500';
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
statusText.textContent = event.data;
|
|
// flash text
|
|
statusText.classList.add('text-blue-400');
|
|
setTimeout(() => statusText.classList.remove('text-blue-400'), 500);
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
wsIndicator.className = 'h-2 w-2 rounded-full bg-red-500';
|
|
setTimeout(connectWS, 2000); // Reconnect
|
|
};
|
|
}
|
|
connectWS();
|
|
|
|
// --- Zone A: Search ---
|
|
searchBtn.addEventListener('click', async () => {
|
|
const query = searchInput.value.trim();
|
|
if (!query) return;
|
|
|
|
searchBtn.disabled = true;
|
|
searchBtn.textContent = '검색 중...';
|
|
resultsContainer.innerHTML = '<div class="col-span-full h-full flex items-center justify-center text-gray-500 animate-pulse">인공지능이 이미지를 탐색하고 선별 중입니다...</div>';
|
|
|
|
try {
|
|
const port = window.location.port ? `:${window.location.port}` : ':3000';
|
|
const res = await fetch(`http://${window.location.hostname}${port}/api/search?q=${encodeURIComponent(query)}`);
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
resultsContainer.innerHTML = `<div class="col-span-full text-red-500 p-4">Error: ${data.error}</div>`;
|
|
} else if (data.recommended && data.recommended.length > 0) {
|
|
resultsContainer.innerHTML = '';
|
|
data.recommended.forEach((item, index) => {
|
|
const delay = index * 100;
|
|
resultsContainer.innerHTML += `
|
|
<div class="recommend-card bg-[#252525] rounded-lg overflow-hidden border border-purple-500/30 hover:border-purple-500 transition-colors cursor-pointer group relative" style="animation-delay: ${delay}ms; opacity:0;">
|
|
<span class="absolute top-2 left-2 bg-purple-600/90 text-xs px-2 py-1 rounded text-white font-medium backdrop-blur-sm z-10 shadow-lg border border-purple-400/50">✨ AI Recommended</span>
|
|
<div class="h-32 w-full bg-black overflow-hidden relative">
|
|
<img src="${item.url}" class="w-full h-full object-cover opacity-80 group-hover:opacity-100 group-hover:scale-105 transition-all duration-300" onerror="this.src='https://via.placeholder.com/300x200?text=Image+Load+Error'" />
|
|
</div>
|
|
<div class="p-3">
|
|
<p class="text-xs text-gray-300 line-clamp-3 leading-relaxed">${item.reason}</p>
|
|
</div>
|
|
<a href="${item.url}" target="_blank" class="absolute inset-0"></a>
|
|
</div>
|
|
`;
|
|
});
|
|
} else {
|
|
resultsContainer.innerHTML = `<div class="col-span-full text-gray-500 p-4">결과를 찾을 수 없습니다.</div>`;
|
|
}
|
|
} catch (err) {
|
|
resultsContainer.innerHTML = `<div class="col-span-full text-red-500 p-4">Exception: ${err.message}</div>`;
|
|
} finally {
|
|
searchBtn.disabled = false;
|
|
searchBtn.textContent = '검색';
|
|
}
|
|
});
|
|
|
|
|
|
// --- Zone B: Upload ---
|
|
dropzone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
dropzone.classList.add('border-purple-500', 'bg-[#303030]');
|
|
});
|
|
|
|
dropzone.addEventListener('dragleave', () => {
|
|
dropzone.classList.remove('border-purple-500', 'bg-[#303030]');
|
|
});
|
|
|
|
dropzone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropzone.classList.remove('border-purple-500', 'bg-[#303030]');
|
|
|
|
if (e.dataTransfer.files.length > 0) {
|
|
uploadFile(e.dataTransfer.files[0]);
|
|
}
|
|
});
|
|
|
|
dropzone.addEventListener('click', () => fileInput.click());
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) {
|
|
uploadFile(e.target.files[0]);
|
|
}
|
|
});
|
|
|
|
async function uploadFile(file) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
statusText.textContent = `Uploading ${file.name}...`;
|
|
|
|
try {
|
|
const port = window.location.port ? `:${window.location.port}` : ':3000';
|
|
const res = await fetch(`http://${window.location.hostname}${port}/api/upload`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
statusText.textContent = `Upload Error: ${data.error}`;
|
|
} else {
|
|
statusText.textContent = `Upload Success: ${data.filename}`;
|
|
}
|
|
} catch (err) {
|
|
statusText.textContent = `Upload Failed: ${err.message}`;
|
|
}
|
|
}
|
|
|
|
|
|
// --- Zone C: Download ---
|
|
async function requestDownload(confirm = false) {
|
|
const url = dlUrl.value.trim();
|
|
if (!url) return;
|
|
|
|
dlBtn.disabled = true;
|
|
dlBtn.textContent = '요청 중...';
|
|
|
|
const payload = {
|
|
url: url,
|
|
start: dlStart.value.trim(),
|
|
end: dlEnd.value.trim()
|
|
};
|
|
|
|
try {
|
|
const port = window.location.port ? `:${window.location.port}` : ':3000';
|
|
const res = await fetch(`http://${window.location.hostname}${port}/api/download${confirm ? '?confirm=true' : ''}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (res.status === 409) {
|
|
// Duplicate!
|
|
confirmModal.classList.remove('hidden');
|
|
confirmModal.classList.add('flex');
|
|
dlBtn.disabled = false;
|
|
dlBtn.textContent = '다운로드 시작';
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
if (data.error) {
|
|
statusText.textContent = `Download Error: ${data.error}`;
|
|
} else {
|
|
// The WS will handle the progress
|
|
dlUrl.value = '';
|
|
dlStart.value = '';
|
|
dlEnd.value = '';
|
|
confirmModal.classList.add('hidden');
|
|
confirmModal.classList.remove('flex');
|
|
}
|
|
} catch (err) {
|
|
statusText.textContent = `Download Request Failed: ${err.message}`;
|
|
} finally {
|
|
dlBtn.disabled = false;
|
|
dlBtn.textContent = '다운로드 시작';
|
|
}
|
|
}
|
|
|
|
dlBtn.addEventListener('click', () => requestDownload(false));
|
|
dlConfirmBtn.addEventListener('click', () => requestDownload(true));
|
|
dlCancelBtn.addEventListener('click', () => {
|
|
confirmModal.classList.add('hidden');
|
|
confirmModal.classList.remove('flex');
|
|
});
|