// Global state
let uploadedPhotos = [];
let hostedPhotoUrls = [];
let currentAnalysis = null;
let detectedPhotoTypes = new Set();
// DOM Elements
const dropZone = document.getElementById('dropZone');
const photoInput = document.getElementById('photoInput');
const photoGrid = document.getElementById('photoGrid');
const resultsSection = document.getElementById('resultsSection');
const emptyState = document.getElementById('emptyState');
// Event Listeners - Initialize after DOM is ready
function initDropZone() {
const dz = document.getElementById('dropZone');
const pInput = document.getElementById('photoInput');
if (!dz || !pInput) {
console.error('Drop zone elements not found');
return;
}
// Store references on elements to prevent duplicate initialization
if (dz._initialized) {
console.log('Drop zone already initialized, skipping');
return;
}
dz._initialized = true;
// Click to browse - use event delegation pattern
dz.addEventListener('click', (e) => {
// Prevent triggering when clicking on child elements like the spinner
if (e.target.closest('#uploadSpinner')) return;
if (e.target.closest('.remove-btn')) return;
e.preventDefault();
e.stopPropagation();
pInput.click();
});
// Drag over - show visual feedback
dz.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
dz.classList.add('drag-over');
});
dz.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
dz.classList.add('drag-over');
});
// Drag leave - remove visual feedback
dz.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
// Only remove if we're actually leaving the dropzone, not entering a child
if (!dz.contains(e.relatedTarget)) {
dz.classList.remove('drag-over');
}
});
// Drop - handle files
dz.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
dz.classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
if (files.length > 0) {
addPhotos(files);
} else {
showToast('Please drop image files only', 'warning');
}
});
// File input change
pInput.addEventListener('change', (e) => {
const files = Array.from(e.target.files);
if (files.length > 0) {
addPhotos(files);
// Reset input so same files can be selected again
pInput.value = '';
}
});
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
const dz = document.getElementById('dropZone');
if (dz) dz.classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
addPhotos(files);
}
function handleFileSelect(e) {
const files = Array.from(e.target.files);
addPhotos(files);
}
async function addPhotos(files) {
if (!files || files.length === 0) return;
uploadedPhotos.push(...files);
renderPhotoGrid();
updateEmptyState();
// Auto-detect photo types first (uses filename heuristics)
setTimeout(() => analyzePhotoTypes(), 100);
// Auto-upload to imgbb if available
if (localStorage.getItem('imgbb_api_key')) {
setTimeout(() => uploadPhotosToImgBB(files), 200);
}
// Trigger OCR analysis if we have photos and API key
if (uploadedPhotos.length > 0 && (localStorage.getItem('openai_api_key') || localStorage.getItem('deepseek_api_key'))) {
setTimeout(() => analyzePhotosWithOCR(), 500);
} else if (uploadedPhotos.length > 0 && !localStorage.getItem('openai_api_key') && !localStorage.getItem('deepseek_api_key')) {
showToast('Add AI API key in Settings for auto-detection', 'warning');
}
}
async function uploadPhotosToImgBB(files) {
const apiKey = localStorage.getItem('imgbb_api_key');
if (!apiKey) {
console.log('No imgBB API key configured, skipping upload');
return;
}
const progressBar = document.getElementById('uploadBar');
const progressContainer = document.getElementById('uploadProgress');
const percentText = document.getElementById('uploadPercent');
progressContainer.classList.remove('hidden');
hostedPhotoUrls = []; // Reset hosted URLs
for (let i = 0; i < files.length; i++) {
const file = files[i];
const base64 = await fileToBase64(file);
const formData = new FormData();
formData.append('image', base64.split(',')[1]); // Remove data:image/*;base64, prefix
formData.append('key', apiKey);
formData.append('name', `${Date.now()}_${file.name.replace(/[^a-zA-Z0-9.]/g, '_')}`);
try {
const response = await fetch('https://api.imgbb.com/1/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success && data.data) {
// Store as object with all relevant URLs from API response
hostedPhotoUrls.push({
id: data.data.id,
url: data.data.url,
displayUrl: data.data.display_url,
viewerUrl: data.data.url_viewer,
deleteUrl: data.data.delete_url,
thumb: data.data.thumb?.url || data.data.url,
medium: data.data.medium?.url || data.data.display_url,
filename: data.data.image?.filename || file.name,
width: data.data.width,
height: data.data.height,
size: data.data.size,
expiration: data.data.expiration
});
console.log('Uploaded:', data.data.url);
} else {
console.error('Upload failed:', data.status, data);
}
} catch (error) {
console.error('Upload failed:', error);
}
const progress = ((i + 1) / files.length) * 100;
progressBar.style.width = `${progress}%`;
percentText.textContent = `${Math.round(progress)}%`;
}
setTimeout(() => {
progressContainer.classList.add('hidden');
if (hostedPhotoUrls.length > 0) {
showToast(`${hostedPhotoUrls.length} photos uploaded to imgBB`, 'success');
console.log('Hosted URLs:', hostedPhotoUrls.map(u => ({ url: u.url, deleteUrl: u.deleteUrl })));
}
}, 500);
}
async function analyzePhotoTypes() {
if (uploadedPhotos.length === 0) return;
detectedPhotoTypes.clear();
// Always do basic filename analysis first
uploadedPhotos.forEach(file => {
const name = file.name.toLowerCase();
if (name.includes('front') || name.includes('cover') || name.includes('front')) detectedPhotoTypes.add('front');
if (name.includes('back') || name.includes('rear')) detectedPhotoTypes.add('back');
if (name.includes('spine')) detectedPhotoTypes.add('spine');
if (name.includes('label') && (name.includes('a') || name.includes('side1') || name.includes('side_1'))) detectedPhotoTypes.add('label_a');
if (name.includes('label') && (name.includes('b') || name.includes('side2') || name.includes('side_2'))) detectedPhotoTypes.add('label_b');
if (name.includes('inner') || name.includes('sleeve')) detectedPhotoTypes.add('inner');
if (name.includes('insert') || name.includes('poster')) detectedPhotoTypes.add('insert');
if (name.includes('hype') || name.includes('sticker')) detectedPhotoTypes.add('hype');
if (name.includes('vinyl') || name.includes('record') || name.includes('disc')) detectedPhotoTypes.add('vinyl');
if (name.includes('corner') || name.includes('edge')) detectedPhotoTypes.add('corners');
if (name.includes('barcode')) detectedPhotoTypes.add('barcode');
if (name.includes('matrix') || name.includes('runout') || name.includes('deadwax')) detectedPhotoTypes.add('deadwax');
});
// Update UI immediately with filename-based detection
renderShotList();
// Try AI detection if available
const service = getAIService();
if (service && service.apiKey) {
showToast('Analyzing photo types with AI...', 'success');
try {
// Analyze each photo to determine what shot it is
for (let i = 0; i < Math.min(uploadedPhotos.length, 4); i++) { // Limit to first 4 to save API calls
try {
const result = await identifyPhotoType(uploadedPhotos[i], service);
if (result && result.type) {
detectedPhotoTypes.add(result.type);
}
} catch (e) {
console.error('Photo type analysis failed for image', i, e);
}
}
renderShotList();
showToast(`Detected ${detectedPhotoTypes.size} shot types`, 'success');
} catch (e) {
console.error('AI photo type detection failed:', e);
}
}
}
async function identifyPhotoType(imageFile, service) {
// Simple heuristic based on filename first
const name = imageFile.name.toLowerCase();
// If we can use AI vision, do so
if (service && service.apiKey) {
try {
const base64 = await fileToBase64Clean(imageFile);
const messages = [
{
role: 'system',
content: `You are analyzing a vinyl record photo. Identify which type of shot this is from this list:
- front: Front cover/album artwork
- back: Back cover/tracklist
- spine: Spine with text
- label_a: Side A label
- label_b: Side B label
- deadwax: Deadwax/runout grooves showing matrix numbers (critical for pressing identification)
- inner: Inner sleeve
- insert: Insert or poster
- hype: Hype sticker on shrink
- vinyl: Vinyl in raking light showing condition
- corners: Close-up of sleeve corners/edges
- barcode: Barcode area
For deadwax photos, look for: hand-etched matrix numbers, stamped codes, "STERLING", "MASTERED BY", plant symbols, or any alphanumeric codes in the runout groove area.
Return ONLY a JSON object: {"type": "one_of_the_above", "confidence": "high|medium|low"}`
},
{
role: 'user',
content: [
{ type: 'text', text: 'What type of record photo is this?' },
{ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64}`, detail: 'low' } }
]
}
];
const response = await fetch(service.baseUrl || 'https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${service.apiKey}`
},
body: JSON.stringify({
model: service.model || 'gpt-4o-mini',
messages: messages,
max_tokens: 100,
temperature: 0.1
})
});
if (response.ok) {
const data = await response.json();
const content = data.choices[0].message.content;
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```|([\s\S]*)/);
const jsonStr = jsonMatch ? (jsonMatch[1] || jsonMatch[2]) : content;
return JSON.parse(jsonStr.trim());
}
} catch (e) {
console.log('AI photo type detection failed, using filename heuristics');
}
}
return null;
}
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result); // Return full data URL including prefix
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// Helper to get clean base64 without data URL prefix
function fileToBase64Clean(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function getAIService() {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
return window.deepseekService;
}
return window.ocrService;
}
// Analysis progress state
let analysisProgressInterval = null;
function updateAnalysisProgress(stage, percent) {
const stageText = document.getElementById('analysisStageText');
const percentText = document.getElementById('analysisPercent');
const progressBar = document.getElementById('analysisBar');
if (stageText) stageText.textContent = stage;
if (percentText) percentText.textContent = `${percent}%`;
if (progressBar) progressBar.style.width = `${percent}%`;
}
function startAnalysisProgressSimulation() {
const stages = [
{ stage: 'Preparing images...', target: 15 },
{ stage: 'Uploading to AI service...', target: 35 },
{ stage: 'Analyzing labels and covers...', target: 60 },
{ stage: 'Extracting text with OCR...', target: 80 },
{ stage: 'Identifying pressing details...', target: 95 },
{ stage: 'Finalizing results...', target: 100 }
];
let currentStage = 0;
let currentPercent = 0;
updateAnalysisProgress(stages[0].stage, 0);
analysisProgressInterval = setInterval(() => {
if (currentStage >= stages.length) {
clearInterval(analysisProgressInterval);
return;
}
const stage = stages[currentStage];
const increment = Math.random() * 3 + 1; // Random increment between 1-4%
currentPercent = Math.min(currentPercent + increment, stage.target);
updateAnalysisProgress(stage.stage, Math.floor(currentPercent));
if (currentPercent >= stage.target && currentStage < stages.length - 1) {
currentStage++;
currentPercent = stage.target;
}
}, 200);
}
function stopAnalysisProgress() {
if (analysisProgressInterval) {
clearInterval(analysisProgressInterval);
analysisProgressInterval = null;
}
updateAnalysisProgress('Complete!', 100);
}
async function analyzePhotosWithOCR() {
const spinner = document.getElementById('uploadSpinner');
const dropZone = document.getElementById('dropZone');
try {
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
// Start progress simulation
startAnalysisProgressSimulation();
// Determine which AI service to use
const provider = localStorage.getItem('ai_provider') || 'openai';
const service = getAIService();
// Update API keys
if (provider === 'openai') {
const apiKey = localStorage.getItem('openai_api_key');
if (!apiKey) throw new Error('OpenAI API key not configured');
window.ocrService.updateApiKey(apiKey);
} else {
const apiKey = localStorage.getItem('deepseek_api_key');
if (!apiKey) throw new Error('DeepSeek API key not configured');
window.deepseekService.updateApiKey(apiKey);
window.deepseekService.updateModel(localStorage.getItem('deepseek_model') || 'deepseek-chat');
}
const result = await service.analyzeRecordImages(uploadedPhotos);
// Complete the progress bar
stopAnalysisProgress();
populateFieldsFromOCR(result);
// Try to fetch additional data from Discogs if available
if (result.artist && result.title && window.discogsService) {
try {
const discogsData = await window.discogsService.searchRelease(
result.artist,
result.title,
result.catalogueNumber
);
if (discogsData) {
populateFieldsFromDiscogs(discogsData);
}
} catch (e) {
console.log('Discogs lookup failed:', e);
}
}
const confidenceMsg = result.confidence === 'high' ? 'Record identified!' :
result.confidence === 'medium' ? 'Record found (verify details)' :
'Partial match found';
showToast(confidenceMsg, result.confidence === 'high' ? 'success' : 'warning');
} catch (error) {
console.error('OCR Error:', error);
if (error.message.includes('API key') || error.message.includes('not configured')) {
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Please configure ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'} API key in Settings`, 'error');
} else {
showToast(`Analysis failed: ${error.message}`, 'error');
}
} finally {
stopAnalysisProgress();
// Small delay to show 100% before hiding
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
// Reset progress for next time
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
function populateFieldsFromDiscogs(discogsData) {
if (!discogsData) return;
// Update year if not already set or if Discogs has better data
const yearInput = document.getElementById('yearInput');
if (discogsData.year && (!yearInput.value || yearInput.value === '[Verify]')) {
yearInput.value = discogsData.year;
yearInput.classList.add('border-orange-500', 'bg-orange-500/10');
setTimeout(() => {
yearInput.classList.remove('border-orange-500', 'bg-orange-500/10');
}, 3000);
}
// Store additional Discogs data for later use
if (discogsData.id) {
window.discogsReleaseId = discogsData.id;
}
// Show Discogs match indicator
let panel = document.getElementById('detectedInfoPanel');
if (panel) {
const discogsBadge = document.createElement('div');
discogsBadge.className = 'mt-2 pt-2 border-t border-green-500/20';
discogsBadge.innerHTML = `
`;
panel.appendChild(discogsBadge);
feather.replace();
}
}
function populateFieldsFromOCR(data) {
if (!data) {
console.error('No OCR data received');
return;
}
const fields = {
'artistInput': data.artist,
'titleInput': data.title,
'catInput': data.catalogueNumber,
'yearInput': data.year
};
let populatedCount = 0;
Object.entries(fields).forEach(([fieldId, value]) => {
const field = document.getElementById(fieldId);
if (field && value && value !== 'null' && value !== 'undefined') {
// Only populate if field is empty or user hasn't manually entered
if (!field.value || field.dataset.userModified !== 'true') {
field.value = value;
field.classList.add('border-green-500', 'bg-green-500/10');
setTimeout(() => {
field.classList.remove('border-green-500', 'bg-green-500/10');
}, 3000);
populatedCount++;
}
}
});
// Store additional data for later use
if (data.label) window.detectedLabel = data.label;
if (data.country) window.detectedCountry = data.country;
if (data.format) window.detectedFormat = data.format;
if (data.genre) window.detectedGenre = data.genre;
if (data.pressingInfo) window.detectedPressingInfo = data.pressingInfo;
if (data.conditionEstimate) window.detectedCondition = data.conditionEstimate;
if (data.notes) window.detectedNotes = data.notes;
// Store pressing identification data
if (data.pressingType) window.detectedPressingType = data.pressingType;
if (data.isFirstPress) window.detectedIsFirstPress = data.isFirstPress;
if (data.reissueYear) window.detectedReissueYear = data.reissueYear;
if (data.originalYear) window.detectedOriginalYear = data.originalYear;
// Update UI to show detected info
updateDetectedInfoPanel(data);
// Scroll to quick details section so user can verify
if (populatedCount > 0) {
const quickDetailsSection = document.querySelector('.md\\:w-80');
if (quickDetailsSection) {
setTimeout(() => {
quickDetailsSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
}
}
}
// Track user modifications to fields
document.addEventListener('DOMContentLoaded', () => {
['artistInput', 'titleInput', 'catInput', 'yearInput'].forEach(id => {
const field = document.getElementById(id);
if (field) {
field.addEventListener('input', () => {
field.dataset.userModified = 'true';
});
}
});
});
function updateDetectedInfoPanel(data) {
if (!data) return;
// Create or update detected info panel
let panel = document.getElementById('detectedInfoPanel');
const parent = document.querySelector('#dropZone')?.parentNode;
if (!parent) return;
if (!panel) {
panel = document.createElement('div');
panel.id = 'detectedInfoPanel';
parent.appendChild(panel);
}
panel.className = 'mt-4 p-4 bg-green-500/10 border border-green-500/30 rounded-lg';
const infoItems = [];
if (data.label && data.label !== 'null') infoItems.push(`Label: ${data.label}`);
if (data.country && data.country !== 'null') infoItems.push(`Country: ${data.country}`);
if (data.format && data.format !== 'null') infoItems.push(`Format: ${data.format}`);
if (data.genre && data.genre !== 'null') infoItems.push(`Genre: ${data.genre}`);
if (data.conditionEstimate && data.conditionEstimate !== 'null') infoItems.push(`Est. Condition: ${data.conditionEstimate}`);
if (data.pressingInfo && data.pressingInfo !== 'null') infoItems.push(`Matrix: ${data.pressingInfo}`);
// Add pressing identification info
if (data.isFirstPress !== undefined) {
const pressBadge = data.isFirstPress
? 'FIRST PRESS '
: data.pressingType === 'reissue'
? 'REISSUE '
: data.pressingType === 'repress'
? 'REPRESS '
: '';
if (pressBadge) infoItems.push(pressBadge);
}
if (data.originalYear && data.originalYear !== data.year) {
infoItems.push(`Original Year: ${data.originalYear}`);
}
if (data.reissueYear && data.reissueYear !== data.year) {
infoItems.push(`Reissue Year: ${data.reissueYear}`);
}
const confidenceColor = data.confidence === 'high' ? 'text-green-400' :
data.confidence === 'medium' ? 'text-yellow-400' : 'text-orange-400';
panel.innerHTML = `
AI Detected Information (${data.confidence || 'unknown'} confidence)
${infoItems.length > 0 ? `
${infoItems.map(item => `
${item}
`).join('')}
` : 'Limited information detected. Try uploading clearer photos of labels and covers.
'}
${data.notes?.length ? `
Additional notes:
${data.notes.map(n => `${n} `).join('')}
` : ''}
`;
feather.replace();
}
function renderPhotoGrid() {
if (uploadedPhotos.length === 0) {
photoGrid.classList.add('hidden');
return;
}
photoGrid.classList.remove('hidden');
photoGrid.innerHTML = uploadedPhotos.map((file, idx) => `
`).join('');
feather.replace();
}
function removePhoto(idx) {
// Also delete from imgBB if hosted
if (hostedPhotoUrls[idx]) {
const hosted = hostedPhotoUrls[idx];
if (hosted.deleteUrl) {
deleteHostedImage(hosted.deleteUrl);
}
hostedPhotoUrls.splice(idx, 1);
}
uploadedPhotos.splice(idx, 1);
renderPhotoGrid();
updateEmptyState();
}
function updateEmptyState() {
if (uploadedPhotos.length > 0) {
// Keep empty state visible until generation
}
}
// Mock Discogs API integration for tracklist lookup
async function fetchDiscogsData(artist, title, catNo) {
// In production, this would call the Discogs API
// For demo, return mock data structure
return {
found: false,
message: 'Connect Discogs API for automatic tracklist lookup'
};
}
// Generate listing analysis
async function generateListing() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
const cost = parseFloat(document.getElementById('costInput').value) || 0;
const goal = document.getElementById('goalSelect').value;
const market = document.getElementById('marketSelect').value;
// Validation
if (uploadedPhotos.length === 0) {
showToast('Please upload at least one photo', 'error');
return;
}
// Simulate analysis delay
dropZone.classList.add('analyzing');
setTimeout(() => {
dropZone.classList.remove('analyzing');
performAnalysis({ artist, title, catNo, year, cost, goal, market });
}, 1500);
}
async function performAnalysis(data) {
const { artist, title, catNo, year, cost, goal, market } = data;
// Determine currency symbol
const currency = market === 'uk' ? '£' : market === 'us' ? 'const container = document.getElementById('titleOptions');
container.innerHTML = titles.map((t, i) => `
${t.chars}/80
${t.text}
${t.style}
`).join('');
}
function selectTitle(el, text) {
document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
// Update clipboard copy
navigator.clipboard.writeText(text);
showToast('Title copied to clipboard!', 'success');
}
function renderPricingStrategy(bin, strategy, comps, currency, goal) {
const container = document.getElementById('pricingStrategy');
const offerSettings = goal === 'max' ? 'Offers: OFF' :
`Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`;
container.innerHTML = `
RECOMMENDED
${currency}${bin}
Buy It Now
Strategy: ${strategy}
Best Offer: ${offerSettings}
Duration: 30 days (GTC)
Sold Comps by Grade
NM/NM-
${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX
${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+
${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})
Based on last 90 days sold listings, same pressing. Prices exclude postage.
`;
}
function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
const container = document.getElementById('feeFloor');
container.innerHTML = `
Your Cost
${currency}${cost.toFixed(2)}
Est. Fees
${currency}${fees.toFixed(2)}
~16% total
Ship + Pack
${currency}${(shipping + packing).toFixed(2)}
Safe Floor Price
${currency}${safeFloor}
Auto-decline below this
`;
}
async function renderHTMLDescription(data, titleObj) {
const { artist, title, catNo, year } = data;
// Use hosted URL if available, otherwise fallback to local object URL
let heroImg = '';
let galleryImages = [];
if (hostedPhotoUrls.length > 0) {
heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url;
galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url);
} else if (uploadedPhotos.length > 0) {
heroImg = URL.createObjectURL(uploadedPhotos[0]);
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
}
// Use OCR-detected values if available
const detectedLabel = window.detectedLabel || '[Verify from photos]';
const detectedCountry = window.detectedCountry || 'UK';
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
const detectedGenre = window.detectedGenre || 'rock';
const detectedCondition = window.detectedCondition || 'VG+/VG+';
const detectedPressingInfo = window.detectedPressingInfo || '';
// Fetch tracklist and detailed info from Discogs if available
let tracklistHtml = '';
let pressingDetailsHtml = '';
let provenanceHtml = '';
if (window.discogsReleaseId && window.discogsService?.key) {
try {
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
if (discogsData && discogsData.tracklist) {
// Build tracklist HTML
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
if (hasSideBreakdown) {
// Group by sides
const sides = {};
discogsData.tracklist.forEach(track => {
const side = track.position ? track.position.charAt(0) : 'Other';
if (!sides[side]) sides[side] = [];
sides[side].push(track);
});
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
Side ${side}
${tracks.map(track => `
${track.position} ${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`).join('');
} else {
// Simple list
tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`;
}
// Build pressing/variation details
const identifiers = discogsData.identifiers || [];
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
pressingDetailsHtml = `
Pressing & Matrix Information
${barcodeInfo ? `
Barcode: ${barcodeInfo.value}
` : ''}
${matrixInfo.map(m => `
${m.type}: ${m.value}${m.description ? ` (${m.description}) ` : ''}
`).join('')}
${pressingInfo.map(p => `
${p.type}: ${p.value}
`).join('')}
${discogsData.notes ? `
${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}
` : ''}
`;
}
// Build provenance data for buyer confidence
const companies = discogsData.companies || [];
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
if (masteredBy || pressedBy || lacquerCut) {
provenanceHtml = `
Provenance & Production
${masteredBy ? `
✓ Mastered at ${masteredBy.name}
` : ''}
${lacquerCut ? `
✓ Lacquer cut at ${lacquerCut.name}
` : ''}
${pressedBy ? `
✓ Pressed at ${pressedBy.name}
` : ''}
${discogsData.num_for_sale ? `
Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs
` : ''}
`;
}
}
} catch (e) {
console.error('Failed to fetch Discogs details for HTML:', e);
}
}
// If no tracklist from Discogs, provide placeholder
if (!tracklistHtml) {
tracklistHtml = `Tracklist verification recommended. Please compare with Discogs entry for accuracy.
`;
}
const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `
`).join('')}
` : '';
const html = `
${galleryHtml}
Original ${detectedCountry} Pressing
${year || '1970s'}
${detectedFormat}
${detectedCondition}
Artist
${artist || 'See title'}
Title
${title || 'See title'}
Label
${detectedLabel}
Catalogue
${catNo || '[See photos]'}
Country
${detectedCountry}
Year
${year || '[Verify]'}
Condition Report
Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]
About This Release
${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]
Tracklist
${tracklistHtml}
${pressingDetailsHtml}
${provenanceHtml}
Packing & Postage
Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.
Combined postage: Discount available for multiple purchases—please request invoice before payment.
Questions? Need more photos?
Message me anytime—happy to provide additional angles, audio clips, or pressing details.
`;
// Store reference to hosted images for potential cleanup
window.currentListingImages = hostedPhotoUrls.map(img => ({
url: img.url,
deleteUrl: img.deleteUrl
}));
document.getElementById('htmlOutput').value = html;
}
function renderTags(artist, title, catNo, year) {
const genre = window.detectedGenre || 'rock';
const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' :
window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp';
const country = window.detectedCountry?.toLowerCase() || 'uk';
const tags = [
artist || 'vinyl',
title || 'record',
format,
'vinyl record',
'original pressing',
`${country} pressing`,
year || 'vintage',
catNo || '',
genre,
genre === 'rock' ? 'prog rock' : genre,
genre === 'rock' ? 'psych' : '',
'collector',
'audiophile',
format === 'lp' ? '12 inch' : format,
'33 rpm',
format === 'lp' ? 'album' : 'single',
'used vinyl',
'graded',
'excellent condition',
'rare vinyl',
'classic rock',
'vintage vinyl',
'record collection',
'music',
'audio',
window.detectedLabel || ''
].filter(Boolean);
const container = document.getElementById('tagsOutput');
container.innerHTML = tags.map(t => `
${t}
`).join('');
}
function renderShotList() {
// Map shot types to display info
const shotDefinitions = [
{ id: 'front', name: 'Front cover (square, well-lit)', critical: true },
{ id: 'back', name: 'Back cover (full shot)', critical: true },
{ id: 'spine', name: 'Spine (readable text)', critical: true },
{ id: 'label_a', name: 'Label Side A (close, legible)', critical: true },
{ id: 'label_b', name: 'Label Side B (close, legible)', critical: true },
{ id: 'deadwax', name: 'Deadwax/runout grooves', critical: true },
{ id: 'inner', name: 'Inner sleeve (both sides)', critical: false },
{ id: 'insert', name: 'Insert/poster if included', critical: false },
{ id: 'hype', name: 'Hype sticker (if present)', critical: false },
{ id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true },
{ id: 'corners', name: 'Sleeve corners/edges detail', critical: false },
{ id: 'barcode', name: 'Barcode area', critical: false }
];
// Check if we have any photos at all
const hasPhotos = uploadedPhotos.length > 0;
const container = document.getElementById('shotList');
container.innerHTML = shotDefinitions.map(shot => {
const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1);
const statusClass = have ? 'completed' : shot.critical ? 'missing' : '';
const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500';
const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300';
const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle';
return `
${shot.name}
${shot.critical && !have ? 'CRITICAL ' : ''}
`}).join('');
feather.replace();
}
function copyHTML() {
const html = document.getElementById('htmlOutput');
html.select();
document.execCommand('copy');
showToast('HTML copied to clipboard!', 'success');
}
function copyTags() {
const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', ');
navigator.clipboard.writeText(tags);
showToast('Tags copied to clipboard!', 'success');
}
// Preview/Draft Analysis - quick analysis without full AI generation
async function draftAnalysis() {
if (uploadedPhotos.length === 0) {
showToast('Upload photos first for preview', 'error');
return;
}
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
// Show loading state
const dropZone = document.getElementById('dropZone');
const spinner = document.getElementById('uploadSpinner');
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
startAnalysisProgressSimulation();
try {
// Try OCR/AI analysis if available
const service = getAIService();
let ocrResult = null;
if (service && service.apiKey && uploadedPhotos.length > 0) {
try {
ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed
populateFieldsFromOCR(ocrResult);
} catch (e) {
console.log('Preview OCR failed:', e);
}
}
// Generate quick preview results
const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || '';
const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || '';
const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist';
const detectedTitle = title || ocrResult?.title || 'Unknown Title';
const baseTitle = `${detectedArtist} - ${detectedTitle}`;
// Generate quick titles
const quickTitles = [
`${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80),
`${baseTitle} Original Pressing Vinyl LP`.substring(0, 80),
`${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80)
].map((t, i) => ({
text: t,
chars: t.length,
style: ['Quick', 'Standard', 'Compact'][i]
}));
// Quick pricing estimate based on condition
const cost = parseFloat(document.getElementById('costInput').value) || 10;
const vinylCond = document.getElementById('vinylConditionInput').value;
const sleeveCond = document.getElementById('sleeveConditionInput').value;
const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 };
const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3;
const estimatedValue = Math.round(cost * Math.max(condMult, 1.5));
const suggestedPrice = Math.round(estimatedValue * 0.9);
// Render preview results
renderTitleOptions(quickTitles);
// Quick pricing card
document.getElementById('pricingStrategy').innerHTML = `
QUICK ESTIMATE
£${suggestedPrice}
Suggested Buy It Now
Est. Value: £${estimatedValue}
Your Cost: £${cost.toFixed(2)}
Condition: ${vinylCond}/${sleeveCond}
Preview Notes
${ocrResult ?
`
✓ AI detected information from photos
` :
`
⚠ Add API key in Settings for auto-detection
`
}
This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.
${ocrResult ? `
Detected from photos:
${ocrResult.artist ? `• Artist: ${ocrResult.artist} ` : ''}
${ocrResult.title ? `• Title: ${ocrResult.title} ` : ''}
${ocrResult.catalogueNumber ? `• Cat#: ${ocrResult.catalogueNumber} ` : ''}
${ocrResult.year ? `• Year: ${ocrResult.year} ` : ''}
` : ''}
`;
// Simple fee floor
const fees = suggestedPrice * 0.16;
const safeFloor = Math.ceil(cost + fees + 6);
document.getElementById('feeFloor').innerHTML = `
Your Cost
£${cost.toFixed(2)}
Est. Fees
£${fees.toFixed(2)}
`;
// Preview HTML description
const previewHtml = `
${detectedArtist} - ${detectedTitle}
${year ? `
Year: ${year}
` : ''}
${catNo ? `
Catalogue #: ${catNo}
` : ''}
Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}
[Full description will be generated with complete market analysis]
`;
const htmlOutput = document.getElementById('htmlOutput');
if (htmlOutput) htmlOutput.value = previewHtml;
// Preview tags
const previewTags = [
detectedArtist,
detectedTitle,
'vinyl',
'record',
vinylCond,
'lp',
year || 'vintage'
].filter(Boolean);
const tagsOutput = document.getElementById('tagsOutput');
if (tagsOutput) {
tagsOutput.innerHTML = previewTags.map(t => `
${t}
`).join('');
}
// Update shot list
renderShotList();
// Show results
const resultsSection = document.getElementById('resultsSection');
const emptyState = document.getElementById('emptyState');
if (resultsSection) resultsSection.classList.remove('hidden');
if (emptyState) emptyState.classList.add('hidden');
if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' });
showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success');
} catch (error) {
console.error('Preview error:', error);
showToast('Preview failed: ' + error.message, 'error');
} finally {
stopAnalysisProgress();
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
async function callAI(messages, temperature = 0.7) {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}`
},
body: JSON.stringify({
model: localStorage.getItem('deepseek_model') || 'deepseek-chat',
messages: messages,
temperature: temperature,
max_tokens: 2000
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'DeepSeek API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`DeepSeek Error: ${error.message}`, 'error');
return null;
}
} else {
// Fallback to OpenAI
const apiKey = localStorage.getItem('openai_api_key');
const model = localStorage.getItem('openai_model') || 'gpt-4o';
const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
if (!apiKey) {
showToast('OpenAI API key not configured. Go to Settings.', 'error');
return null;
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: messages,
temperature: temperature,
max_tokens: maxTokens
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`OpenAI Error: ${error.message}`, 'error');
return null;
}
}
}
// Legacy alias for backward compatibility
async function callOpenAI(messages, temperature = 0.7) {
return callAI(messages, temperature);
}
// Delete hosted image from imgBB
async function deleteHostedImage(deleteUrl) {
if (!deleteUrl) return false;
try {
const response = await fetch(deleteUrl, { method: 'GET' });
// imgBB delete URLs work via GET request
return response.ok;
} catch (error) {
console.error('Failed to delete image:', error);
return false;
}
}
// Get hosted photo URLs for eBay HTML description
function getHostedPhotoUrlsForEbay() {
return hostedPhotoUrls.map(img => ({
full: img.url,
display: img.displayUrl || img.url,
thumb: img.thumb,
medium: img.medium,
viewer: img.viewerUrl
}));
}
async function generateListingWithAI() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
if (!artist || !title) {
showToast('Please enter at least artist and title', 'error');
return;
}
const messages = [
{
role: 'system',
content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
},
{
role: 'user',
content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
}
];
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success');
const result = await callAI(messages, 0.7);
if (result) {
try {
const data = JSON.parse(result);
// Populate the UI with AI-generated content
if (data.titles) {
renderTitleOptions(data.titles.map(t => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: 'AI Generated'
})));
}
if (data.description) {
document.getElementById('htmlOutput').value = data.description;
}
if (data.tags) {
const tagsContainer = document.getElementById('tagsOutput');
tagsContainer.innerHTML = data.tags.map(t => `
${t}
`).join('');
}
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
showToast('AI listing generated!', 'success');
} catch (e) {
// If not valid JSON, treat as plain text description
document.getElementById('htmlOutput').value = result;
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
}
}
}
function requestHelp() {
alert(`VINYL PHOTO GUIDE:
ESSENTIAL SHOTS (need these):
• Front cover - square, no glare, color accurate
• Back cover - full frame, readable text
• Both labels - close enough to read all text
• Deadwax/runout - for pressing identification
CONDITION SHOTS:
• Vinyl in raking light at angle (shows scratches)
• Sleeve edges and corners
• Any flaws clearly documented
OPTIONARY BUT HELPFUL:
• Inner sleeve condition
• Inserts, posters, extras
• Hype stickers
• Barcode area
TIPS:
- Use natural daylight or 5500K bulbs
- Avoid flash directly on glossy sleeves
- Include scale reference if unusual size
- Photograph flaws honestly - reduces returns`);
}
function showToast(message, type = 'success') {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const iconMap = {
success: 'check',
error: 'alert-circle',
warning: 'alert-triangle'
};
const colorMap = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400'
};
const toast = document.createElement('div');
toast.className = `toast ${type} flex items-center gap-3`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
feather.replace();
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Cleanup function to delete all hosted images for current listing
async function cleanupHostedImages() {
if (window.currentListingImages) {
for (const img of window.currentListingImages) {
if (img.deleteUrl) {
await deleteHostedImage(img.deleteUrl);
}
}
window.currentListingImages = [];
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('VinylVault Pro initialized');
// Initialize drop zone
initDropZone();
// Attach event listeners to buttons
const generateBtn = document.getElementById('generateListingBtn');
if (generateBtn) {
generateBtn.addEventListener('click', generateListing);
}
const draftBtn = document.getElementById('draftAnalysisBtn');
if (draftBtn) {
draftBtn.addEventListener('click', draftAnalysis);
}
const helpBtn = document.getElementById('requestHelpBtn');
if (helpBtn) {
helpBtn.addEventListener('click', requestHelp);
}
const copyHTMLBtn = document.getElementById('copyHTMLBtn');
if (copyHTMLBtn) {
copyHTMLBtn.addEventListener('click', copyHTML);
}
const copyTagsBtn = document.getElementById('copyTagsBtn');
if (copyTagsBtn) {
copyTagsBtn.addEventListener('click', copyTags);
}
const analyzePhotoBtn = document.getElementById('analyzePhotoTypesBtn');
if (analyzePhotoBtn) {
analyzePhotoBtn.addEventListener('click', analyzePhotoTypes);
}
// Clear Collection Import Banner listeners
const clearCollectionBtn = document.querySelector('#collectionBanner button');
if (clearCollectionBtn) {
clearCollectionBtn.addEventListener('click', clearCollectionImport);
}
// Warn about unsaved changes when leaving page with hosted images
window.addEventListener('beforeunload', (e) => {
if (hostedPhotoUrls.length > 0 && !window.listingPublished) {
// Optional: could add cleanup here or warn user
}
});
});
// Collection Import functions (defined here to avoid reference errors)
function clearCollectionImport() {
sessionStorage.removeItem('collectionListingRecord');
const banner = document.getElementById('collectionBanner');
if (banner) {
banner.classList.add('hidden');
}
showToast('Collection import cleared', 'success');
}
function checkCollectionImport() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('fromCollection') === 'true') {
const recordData = sessionStorage.getItem('collectionListingRecord');
if (recordData) {
const record = JSON.parse(recordData);
populateFieldsFromCollection(record);
const banner = document.getElementById('collectionBanner');
if (banner) {
banner.classList.remove('hidden');
}
const indicator = document.getElementById('collectionDataIndicator');
if (indicator) {
indicator.classList.remove('hidden');
}
}
}
}
function populateFieldsFromCollection(record) {
if (!record) return;
const fields = {
'artistInput': record.artist,
'titleInput': record.title,
'catInput': record.catalogueNumber || record.matrixNotes,
'yearInput': record.year,
'costInput': record.purchasePrice,
'daysOwnedInput': record.daysOwned
};
Object.entries(fields).forEach(([fieldId, value]) => {
const field = document.getElementById(fieldId);
if (field && value) {
field.value = value;
}
});
// Set conditions if available
if (record.conditionVinyl) {
const vinylCondition = document.getElementById('vinylConditionInput');
if (vinylCondition) vinylCondition.value = record.conditionVinyl;
}
if (record.conditionSleeve) {
const sleeveCondition = document.getElementById('sleeveConditionInput');
if (sleeveCondition) sleeveCondition.value = record.conditionSleeve;
}
showToast(`Loaded ${record.artist} - ${record.title} from collection`, 'success');
}
// Call check on load
checkCollectionImport();
: '€';
// Mock comp research results
const comps = {
nm: { low: 45, high: 65, median: 52 },
vgplus: { low: 28, high: 42, median: 34 },
vg: { low: 15, high: 25, median: 19 }
};
// Calculate recommended price based on goal
let recommendedBin, strategy;
switch(goal) {
case 'quick':
recommendedBin = Math.round(comps.vgplus.low * 0.9);
strategy = 'BIN + Best Offer (aggressive)';
break;
case 'max':
recommendedBin = Math.round(comps.nm.high * 1.1);
strategy = 'BIN only, no offers, long duration';
break;
default:
recommendedBin = comps.vgplus.median;
strategy = 'BIN + Best Offer (standard)';
}
// Fee calculation (eBay UK approx)
const ebayFeeRate = 0.13; // 13% final value fee
const paypalRate = 0.029; // 2.9% + 30p
const fixedFee = 0.30;
const shippingCost = 4.50; // Estimated
const packingCost = 1.50;
const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee;
const breakEven = cost + totalFees + shippingCost + packingCost;
const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer
// Generate titles
const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`;
const titles = generateTitles(baseTitle, catNo, year, goal);
// Render results
renderTitleOptions(titles);
renderPricingStrategy(recommendedBin, strategy, comps, currency, goal);
renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency);
await renderHTMLDescription(data, titles[0]);
renderTags(artist, title, catNo, year);
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
currentAnalysis = {
titles, recommendedBin, strategy, breakEven, safeFloor, currency
};
}
function generateTitles(base, catNo, year, goal) {
const titles = [];
const cat = catNo || 'CAT#';
const yr = year || 'YEAR';
const country = window.detectedCountry || 'UK';
const genre = window.detectedGenre || 'Rock';
const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP';
// Option 1: Classic collector focus
titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`);
// Option 2: Condition forward
titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`);
// Option 3: Rarity/hype with detected genre
titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`);
// Option 4: Clean searchable
titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`);
// Option 5: Genre tagged
titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`);
return titles.map((t, i) => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i]
}));
}
function renderTitleOptions(titles) {
const container = document.getElementById('titleOptions');
container.innerHTML = titles.map((t, i) => `
${t.chars}/80
${t.text}
${t.style}
`).join('');
}
function selectTitle(el, text) {
document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
// Update clipboard copy
navigator.clipboard.writeText(text);
showToast('Title copied to clipboard!', 'success');
}
function renderPricingStrategy(bin, strategy, comps, currency, goal) {
const container = document.getElementById('pricingStrategy');
const offerSettings = goal === 'max' ? 'Offers: OFF' :
`Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`;
container.innerHTML = `
RECOMMENDED
${currency}${bin}
Buy It Now
Strategy: ${strategy}
Best Offer: ${offerSettings}
Duration: 30 days (GTC)
Sold Comps by Grade
NM/NM-
${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX
${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+
${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})
Based on last 90 days sold listings, same pressing. Prices exclude postage.
`;
}
function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
const container = document.getElementById('feeFloor');
container.innerHTML = `
Your Cost
${currency}${cost.toFixed(2)}
Est. Fees
${currency}${fees.toFixed(2)}
~16% total
Ship + Pack
${currency}${(shipping + packing).toFixed(2)}
Safe Floor Price
${currency}${safeFloor}
Auto-decline below this
`;
}
async function renderHTMLDescription(data, titleObj) {
const { artist, title, catNo, year } = data;
// Use hosted URL if available, otherwise fallback to local object URL
let heroImg = '';
let galleryImages = [];
if (hostedPhotoUrls.length > 0) {
heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url;
galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url);
} else if (uploadedPhotos.length > 0) {
heroImg = URL.createObjectURL(uploadedPhotos[0]);
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
}
// Use OCR-detected values if available
const detectedLabel = window.detectedLabel || '[Verify from photos]';
const detectedCountry = window.detectedCountry || 'UK';
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
const detectedGenre = window.detectedGenre || 'rock';
const detectedCondition = window.detectedCondition || 'VG+/VG+';
const detectedPressingInfo = window.detectedPressingInfo || '';
// Fetch tracklist and detailed info from Discogs if available
let tracklistHtml = '';
let pressingDetailsHtml = '';
let provenanceHtml = '';
if (window.discogsReleaseId && window.discogsService?.key) {
try {
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
if (discogsData && discogsData.tracklist) {
// Build tracklist HTML
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
if (hasSideBreakdown) {
// Group by sides
const sides = {};
discogsData.tracklist.forEach(track => {
const side = track.position ? track.position.charAt(0) : 'Other';
if (!sides[side]) sides[side] = [];
sides[side].push(track);
});
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
Side ${side}
${tracks.map(track => `
${track.position} ${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`).join('');
} else {
// Simple list
tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`;
}
// Build pressing/variation details
const identifiers = discogsData.identifiers || [];
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
pressingDetailsHtml = `
Pressing & Matrix Information
${barcodeInfo ? `
Barcode: ${barcodeInfo.value}
` : ''}
${matrixInfo.map(m => `
${m.type}: ${m.value}${m.description ? ` (${m.description}) ` : ''}
`).join('')}
${pressingInfo.map(p => `
${p.type}: ${p.value}
`).join('')}
${discogsData.notes ? `
${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}
` : ''}
`;
}
// Build provenance data for buyer confidence
const companies = discogsData.companies || [];
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
if (masteredBy || pressedBy || lacquerCut) {
provenanceHtml = `
Provenance & Production
${masteredBy ? `
✓ Mastered at ${masteredBy.name}
` : ''}
${lacquerCut ? `
✓ Lacquer cut at ${lacquerCut.name}
` : ''}
${pressedBy ? `
✓ Pressed at ${pressedBy.name}
` : ''}
${discogsData.num_for_sale ? `
Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs
` : ''}
`;
}
}
} catch (e) {
console.error('Failed to fetch Discogs details for HTML:', e);
}
}
// If no tracklist from Discogs, provide placeholder
if (!tracklistHtml) {
tracklistHtml = `Tracklist verification recommended. Please compare with Discogs entry for accuracy.
`;
}
const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `
`).join('')}
` : '';
const html = `
${galleryHtml}
Original ${detectedCountry} Pressing
${year || '1970s'}
${detectedFormat}
${detectedCondition}
Artist
${artist || 'See title'}
Title
${title || 'See title'}
Label
${detectedLabel}
Catalogue
${catNo || '[See photos]'}
Country
${detectedCountry}
Year
${year || '[Verify]'}
Condition Report
Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]
About This Release
${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]
Tracklist
${tracklistHtml}
${pressingDetailsHtml}
${provenanceHtml}
Packing & Postage
Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.
Combined postage: Discount available for multiple purchases—please request invoice before payment.
Questions? Need more photos?
Message me anytime—happy to provide additional angles, audio clips, or pressing details.
`;
// Store reference to hosted images for potential cleanup
window.currentListingImages = hostedPhotoUrls.map(img => ({
url: img.url,
deleteUrl: img.deleteUrl
}));
document.getElementById('htmlOutput').value = html;
}
function renderTags(artist, title, catNo, year) {
const genre = window.detectedGenre || 'rock';
const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' :
window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp';
const country = window.detectedCountry?.toLowerCase() || 'uk';
const tags = [
artist || 'vinyl',
title || 'record',
format,
'vinyl record',
'original pressing',
`${country} pressing`,
year || 'vintage',
catNo || '',
genre,
genre === 'rock' ? 'prog rock' : genre,
genre === 'rock' ? 'psych' : '',
'collector',
'audiophile',
format === 'lp' ? '12 inch' : format,
'33 rpm',
format === 'lp' ? 'album' : 'single',
'used vinyl',
'graded',
'excellent condition',
'rare vinyl',
'classic rock',
'vintage vinyl',
'record collection',
'music',
'audio',
window.detectedLabel || ''
].filter(Boolean);
const container = document.getElementById('tagsOutput');
container.innerHTML = tags.map(t => `
${t}
`).join('');
}
function renderShotList() {
// Map shot types to display info
const shotDefinitions = [
{ id: 'front', name: 'Front cover (square, well-lit)', critical: true },
{ id: 'back', name: 'Back cover (full shot)', critical: true },
{ id: 'spine', name: 'Spine (readable text)', critical: true },
{ id: 'label_a', name: 'Label Side A (close, legible)', critical: true },
{ id: 'label_b', name: 'Label Side B (close, legible)', critical: true },
{ id: 'deadwax', name: 'Deadwax/runout grooves', critical: true },
{ id: 'inner', name: 'Inner sleeve (both sides)', critical: false },
{ id: 'insert', name: 'Insert/poster if included', critical: false },
{ id: 'hype', name: 'Hype sticker (if present)', critical: false },
{ id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true },
{ id: 'corners', name: 'Sleeve corners/edges detail', critical: false },
{ id: 'barcode', name: 'Barcode area', critical: false }
];
// Check if we have any photos at all
const hasPhotos = uploadedPhotos.length > 0;
const container = document.getElementById('shotList');
container.innerHTML = shotDefinitions.map(shot => {
const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1);
const statusClass = have ? 'completed' : shot.critical ? 'missing' : '';
const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500';
const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300';
const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle';
return `
${shot.name}
${shot.critical && !have ? 'CRITICAL ' : ''}
`}).join('');
feather.replace();
}
function copyHTML() {
const html = document.getElementById('htmlOutput');
html.select();
document.execCommand('copy');
showToast('HTML copied to clipboard!', 'success');
}
function copyTags() {
const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', ');
navigator.clipboard.writeText(tags);
showToast('Tags copied to clipboard!', 'success');
}
async function draftAnalysis() {
if (uploadedPhotos.length === 0) {
showToast('Upload photos first for preview', 'error');
return;
}
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
// Show loading state
const dropZone = document.getElementById('dropZone');
const spinner = document.getElementById('uploadSpinner');
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
startAnalysisProgressSimulation();
try {
// Try OCR/AI analysis if available
const service = getAIService();
let ocrResult = null;
if (service && service.apiKey && uploadedPhotos.length > 0) {
try {
ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed
populateFieldsFromOCR(ocrResult);
} catch (e) {
console.log('Preview OCR failed:', e);
}
}
// Generate quick preview results
const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || '';
const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || '';
const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist';
const detectedTitle = title || ocrResult?.title || 'Unknown Title';
const baseTitle = `${detectedArtist} - ${detectedTitle}`;
// Generate quick titles
const quickTitles = [
`${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80),
`${baseTitle} Original Pressing Vinyl LP`.substring(0, 80),
`${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80)
].map((t, i) => ({
text: t,
chars: t.length,
style: ['Quick', 'Standard', 'Compact'][i]
}));
// Quick pricing estimate based on condition
const cost = parseFloat(document.getElementById('costInput').value) || 10;
const vinylCond = document.getElementById('vinylConditionInput').value;
const sleeveCond = document.getElementById('sleeveConditionInput').value;
const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 };
const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3;
const estimatedValue = Math.round(cost * Math.max(condMult, 1.5));
const suggestedPrice = Math.round(estimatedValue * 0.9);
// Render preview results
renderTitleOptions(quickTitles);
// Quick pricing card
document.getElementById('pricingStrategy').innerHTML = `
QUICK ESTIMATE
£${suggestedPrice}
Suggested Buy It Now
Est. Value: £${estimatedValue}
Your Cost: £${cost.toFixed(2)}
Condition: ${vinylCond}/${sleeveCond}
Preview Notes
${ocrResult ?
`
✓ AI detected information from photos
` :
`
⚠ Add API key in Settings for auto-detection
`
}
This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.
${ocrResult ? `
Detected from photos:
${ocrResult.artist ? `• Artist: ${ocrResult.artist} ` : ''}
${ocrResult.title ? `• Title: ${ocrResult.title} ` : ''}
${ocrResult.catalogueNumber ? `• Cat#: ${ocrResult.catalogueNumber} ` : ''}
${ocrResult.year ? `• Year: ${ocrResult.year} ` : ''}
` : ''}
`;
// Simple fee floor
const fees = suggestedPrice * 0.16;
const safeFloor = Math.ceil(cost + fees + 6);
document.getElementById('feeFloor').innerHTML = `
Your Cost
£${cost.toFixed(2)}
Est. Fees
£${fees.toFixed(2)}
`;
// Preview HTML description
const previewHtml = `
${detectedArtist} - ${detectedTitle}
${year ? `
Year: ${year}
` : ''}
${catNo ? `
Catalogue #: ${catNo}
` : ''}
Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}
[Full description will be generated with complete market analysis]
`;
document.getElementById('htmlOutput').value = previewHtml;
// Preview tags
const previewTags = [
detectedArtist,
detectedTitle,
'vinyl',
'record',
vinylCond,
'lp',
year || 'vintage'
].filter(Boolean);
document.getElementById('tagsOutput').innerHTML = previewTags.map(t => `
${t}
`).join('');
// Update shot list
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success');
} catch (error) {
console.error('Preview error:', error);
showToast('Preview failed: ' + error.message, 'error');
} finally {
stopAnalysisProgress();
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
async function callAI(messages, temperature = 0.7) {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}`
},
body: JSON.stringify({
model: localStorage.getItem('deepseek_model') || 'deepseek-chat',
messages: messages,
temperature: temperature,
max_tokens: 2000
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'DeepSeek API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`DeepSeek Error: ${error.message}`, 'error');
return null;
}
} else {
// Fallback to OpenAI
const apiKey = localStorage.getItem('openai_api_key');
const model = localStorage.getItem('openai_model') || 'gpt-4o';
const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
if (!apiKey) {
showToast('OpenAI API key not configured. Go to Settings.', 'error');
return null;
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: messages,
temperature: temperature,
max_tokens: maxTokens
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`OpenAI Error: ${error.message}`, 'error');
return null;
}
}
}
// Legacy alias for backward compatibility
async function callOpenAI(messages, temperature = 0.7) {
return callAI(messages, temperature);
}
// Delete hosted image from imgBB
async function deleteHostedImage(deleteUrl) {
if (!deleteUrl) return false;
try {
const response = await fetch(deleteUrl, { method: 'GET' });
// imgBB delete URLs work via GET request
return response.ok;
} catch (error) {
console.error('Failed to delete image:', error);
return false;
}
}
// Get hosted photo URLs for eBay HTML description
function getHostedPhotoUrlsForEbay() {
return hostedPhotoUrls.map(img => ({
full: img.url,
display: img.displayUrl || img.url,
thumb: img.thumb,
medium: img.medium,
viewer: img.viewerUrl
}));
}
async function generateListingWithAI() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
if (!artist || !title) {
showToast('Please enter at least artist and title', 'error');
return;
}
const messages = [
{
role: 'system',
content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
},
{
role: 'user',
content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
}
];
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success');
const result = await callAI(messages, 0.7);
if (result) {
try {
const data = JSON.parse(result);
// Populate the UI with AI-generated content
if (data.titles) {
renderTitleOptions(data.titles.map(t => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: 'AI Generated'
})));
}
if (data.description) {
document.getElementById('htmlOutput').value = data.description;
}
if (data.tags) {
const tagsContainer = document.getElementById('tagsOutput');
tagsContainer.innerHTML = data.tags.map(t => `
${t}
`).join('');
}
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
showToast('AI listing generated!', 'success');
} catch (e) {
// If not valid JSON, treat as plain text description
document.getElementById('htmlOutput').value = result;
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
}
}
}
function requestHelp() {
alert(`VINYL PHOTO GUIDE:
ESSENTIAL SHOTS (need these):
• Front cover - square, no glare, color accurate
• Back cover - full frame, readable text
• Both labels - close enough to read all text
• Deadwax/runout - for pressing identification
CONDITION SHOTS:
• Vinyl in raking light at angle (shows scratches)
• Sleeve edges and corners
• Any flaws clearly documented
OPTIONARY BUT HELPFUL:
• Inner sleeve condition
• Inserts, posters, extras
• Hype stickers
• Barcode area
TIPS:
- Use natural daylight or 5500K bulbs
- Avoid flash directly on glossy sleeves
- Include scale reference if unusual size
- Photograph flaws honestly - reduces returns`);
}
function showToast(message, type = 'success') {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const iconMap = {
success: 'check',
error: 'alert-circle',
warning: 'alert-triangle'
};
const colorMap = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400'
};
const toast = document.createElement('div');
toast.className = `toast ${type} flex items-center gap-3`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
feather.replace();
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Cleanup function to delete all hosted images for current listing
async function cleanupHostedImages() {
if (window.currentListingImages) {
for (const img of window.currentListingImages) {
if (img.deleteUrl) {
await deleteHostedImage(img.deleteUrl);
}
}
window.currentListingImages = [];
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('VinylVault Pro initialized');
// Initialize drop zone
initDropZone();
// Warn about unsaved changes when leaving page with hosted images
window.addEventListener('beforeunload', (e) => {
if (hostedPhotoUrls.length > 0 && !window.listingPublished) {
// Optional: could add cleanup here or warn user
}
});
});
function renderTitleOptions(titles) {
const container = document.getElementById('titleOptions');
container.innerHTML = titles.map((t, i) => `
${t.chars}/80
${t.text}
${t.style}
`).join('');
}
function selectTitle(el, text) {
document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
// Update clipboard copy
navigator.clipboard.writeText(text);
showToast('Title copied to clipboard!', 'success');
}
function renderPricingStrategy(bin, strategy, comps, currency, goal) {
const container = document.getElementById('pricingStrategy');
const offerSettings = goal === 'max' ? 'Offers: OFF' :
`Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`;
container.innerHTML = `
RECOMMENDED
${currency}${bin}
Buy It Now
Strategy: ${strategy}
Best Offer: ${offerSettings}
Duration: 30 days (GTC)
Sold Comps by Grade
NM/NM-
${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX
${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+
${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})
Based on last 90 days sold listings, same pressing. Prices exclude postage.
`;
}
function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
const container = document.getElementById('feeFloor');
container.innerHTML = `
Your Cost
${currency}${cost.toFixed(2)}
Est. Fees
${currency}${fees.toFixed(2)}
~16% total
Ship + Pack
${currency}${(shipping + packing).toFixed(2)}
Safe Floor Price
${currency}${safeFloor}
Auto-decline below this
`;
}
async function renderHTMLDescription(data, titleObj) {
const { artist, title, catNo, year } = data;
// Use hosted URL if available, otherwise fallback to local object URL
let heroImg = '';
let galleryImages = [];
if (hostedPhotoUrls.length > 0) {
heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url;
galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url);
} else if (uploadedPhotos.length > 0) {
heroImg = URL.createObjectURL(uploadedPhotos[0]);
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
}
// Use OCR-detected values if available
const detectedLabel = window.detectedLabel || '[Verify from photos]';
const detectedCountry = window.detectedCountry || 'UK';
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
const detectedGenre = window.detectedGenre || 'rock';
const detectedCondition = window.detectedCondition || 'VG+/VG+';
const detectedPressingInfo = window.detectedPressingInfo || '';
// Fetch tracklist and detailed info from Discogs if available
let tracklistHtml = '';
let pressingDetailsHtml = '';
let provenanceHtml = '';
if (window.discogsReleaseId && window.discogsService?.key) {
try {
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
if (discogsData && discogsData.tracklist) {
// Build tracklist HTML
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
if (hasSideBreakdown) {
// Group by sides
const sides = {};
discogsData.tracklist.forEach(track => {
const side = track.position ? track.position.charAt(0) : 'Other';
if (!sides[side]) sides[side] = [];
sides[side].push(track);
});
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
Side ${side}
${tracks.map(track => `
${track.position} ${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`).join('');
} else {
// Simple list
tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`;
}
// Build pressing/variation details
const identifiers = discogsData.identifiers || [];
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
pressingDetailsHtml = `
Pressing & Matrix Information
${barcodeInfo ? `
Barcode: ${barcodeInfo.value}
` : ''}
${matrixInfo.map(m => `
${m.type}: ${m.value}${m.description ? ` (${m.description}) ` : ''}
`).join('')}
${pressingInfo.map(p => `
${p.type}: ${p.value}
`).join('')}
${discogsData.notes ? `
${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}
` : ''}
`;
}
// Build provenance data for buyer confidence
const companies = discogsData.companies || [];
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
if (masteredBy || pressedBy || lacquerCut) {
provenanceHtml = `
Provenance & Production
${masteredBy ? `
✓ Mastered at ${masteredBy.name}
` : ''}
${lacquerCut ? `
✓ Lacquer cut at ${lacquerCut.name}
` : ''}
${pressedBy ? `
✓ Pressed at ${pressedBy.name}
` : ''}
${discogsData.num_for_sale ? `
Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs
` : ''}
`;
}
}
} catch (e) {
console.error('Failed to fetch Discogs details for HTML:', e);
}
}
// If no tracklist from Discogs, provide placeholder
if (!tracklistHtml) {
tracklistHtml = `Tracklist verification recommended. Please compare with Discogs entry for accuracy.
`;
}
const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `
`).join('')}
` : '';
const html = `
${galleryHtml}
Original ${detectedCountry} Pressing
${year || '1970s'}
${detectedFormat}
${detectedCondition}
Artist
${artist || 'See title'}
Title
${title || 'See title'}
Label
${detectedLabel}
Catalogue
${catNo || '[See photos]'}
Country
${detectedCountry}
Year
${year || '[Verify]'}
Condition Report
Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]
About This Release
${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]
Tracklist
${tracklistHtml}
${pressingDetailsHtml}
${provenanceHtml}
Packing & Postage
Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.
Combined postage: Discount available for multiple purchases—please request invoice before payment.
Questions? Need more photos?
Message me anytime—happy to provide additional angles, audio clips, or pressing details.
`;
// Store reference to hosted images for potential cleanup
window.currentListingImages = hostedPhotoUrls.map(img => ({
url: img.url,
deleteUrl: img.deleteUrl
}));
document.getElementById('htmlOutput').value = html;
}
function renderTags(artist, title, catNo, year) {
const genre = window.detectedGenre || 'rock';
const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' :
window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp';
const country = window.detectedCountry?.toLowerCase() || 'uk';
const tags = [
artist || 'vinyl',
title || 'record',
format,
'vinyl record',
'original pressing',
`${country} pressing`,
year || 'vintage',
catNo || '',
genre,
genre === 'rock' ? 'prog rock' : genre,
genre === 'rock' ? 'psych' : '',
'collector',
'audiophile',
format === 'lp' ? '12 inch' : format,
'33 rpm',
format === 'lp' ? 'album' : 'single',
'used vinyl',
'graded',
'excellent condition',
'rare vinyl',
'classic rock',
'vintage vinyl',
'record collection',
'music',
'audio',
window.detectedLabel || ''
].filter(Boolean);
const container = document.getElementById('tagsOutput');
container.innerHTML = tags.map(t => `
${t}
`).join('');
}
function renderShotList() {
// Map shot types to display info
const shotDefinitions = [
{ id: 'front', name: 'Front cover (square, well-lit)', critical: true },
{ id: 'back', name: 'Back cover (full shot)', critical: true },
{ id: 'spine', name: 'Spine (readable text)', critical: true },
{ id: 'label_a', name: 'Label Side A (close, legible)', critical: true },
{ id: 'label_b', name: 'Label Side B (close, legible)', critical: true },
{ id: 'deadwax', name: 'Deadwax/runout grooves', critical: true },
{ id: 'inner', name: 'Inner sleeve (both sides)', critical: false },
{ id: 'insert', name: 'Insert/poster if included', critical: false },
{ id: 'hype', name: 'Hype sticker (if present)', critical: false },
{ id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true },
{ id: 'corners', name: 'Sleeve corners/edges detail', critical: false },
{ id: 'barcode', name: 'Barcode area', critical: false }
];
// Check if we have any photos at all
const hasPhotos = uploadedPhotos.length > 0;
const container = document.getElementById('shotList');
container.innerHTML = shotDefinitions.map(shot => {
const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1);
const statusClass = have ? 'completed' : shot.critical ? 'missing' : '';
const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500';
const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300';
const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle';
return `
${shot.name}
${shot.critical && !have ? 'CRITICAL ' : ''}
`}).join('');
feather.replace();
}
function copyHTML() {
const html = document.getElementById('htmlOutput');
html.select();
document.execCommand('copy');
showToast('HTML copied to clipboard!', 'success');
}
function copyTags() {
const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', ');
navigator.clipboard.writeText(tags);
showToast('Tags copied to clipboard!', 'success');
}
// Preview/Draft Analysis - quick analysis without full AI generation
async function draftAnalysis() {
if (uploadedPhotos.length === 0) {
showToast('Upload photos first for preview', 'error');
return;
}
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
// Show loading state
const dropZone = document.getElementById('dropZone');
const spinner = document.getElementById('uploadSpinner');
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
startAnalysisProgressSimulation();
try {
// Try OCR/AI analysis if available
const service = getAIService();
let ocrResult = null;
if (service && service.apiKey && uploadedPhotos.length > 0) {
try {
ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed
populateFieldsFromOCR(ocrResult);
} catch (e) {
console.log('Preview OCR failed:', e);
}
}
// Generate quick preview results
const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || '';
const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || '';
const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist';
const detectedTitle = title || ocrResult?.title || 'Unknown Title';
const baseTitle = `${detectedArtist} - ${detectedTitle}`;
// Generate quick titles
const quickTitles = [
`${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80),
`${baseTitle} Original Pressing Vinyl LP`.substring(0, 80),
`${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80)
].map((t, i) => ({
text: t,
chars: t.length,
style: ['Quick', 'Standard', 'Compact'][i]
}));
// Quick pricing estimate based on condition
const cost = parseFloat(document.getElementById('costInput').value) || 10;
const vinylCond = document.getElementById('vinylConditionInput').value;
const sleeveCond = document.getElementById('sleeveConditionInput').value;
const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 };
const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3;
const estimatedValue = Math.round(cost * Math.max(condMult, 1.5));
const suggestedPrice = Math.round(estimatedValue * 0.9);
// Render preview results
renderTitleOptions(quickTitles);
// Quick pricing card
document.getElementById('pricingStrategy').innerHTML = `
QUICK ESTIMATE
£${suggestedPrice}
Suggested Buy It Now
Est. Value: £${estimatedValue}
Your Cost: £${cost.toFixed(2)}
Condition: ${vinylCond}/${sleeveCond}
Preview Notes
${ocrResult ?
`
✓ AI detected information from photos
` :
`
⚠ Add API key in Settings for auto-detection
`
}
This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.
${ocrResult ? `
Detected from photos:
${ocrResult.artist ? `• Artist: ${ocrResult.artist} ` : ''}
${ocrResult.title ? `• Title: ${ocrResult.title} ` : ''}
${ocrResult.catalogueNumber ? `• Cat#: ${ocrResult.catalogueNumber} ` : ''}
${ocrResult.year ? `• Year: ${ocrResult.year} ` : ''}
` : ''}
`;
// Simple fee floor
const fees = suggestedPrice * 0.16;
const safeFloor = Math.ceil(cost + fees + 6);
document.getElementById('feeFloor').innerHTML = `
Your Cost
£${cost.toFixed(2)}
Est. Fees
£${fees.toFixed(2)}
`;
// Preview HTML description
const previewHtml = `
${detectedArtist} - ${detectedTitle}
${year ? `
Year: ${year}
` : ''}
${catNo ? `
Catalogue #: ${catNo}
` : ''}
Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}
[Full description will be generated with complete market analysis]
`;
const htmlOutput = document.getElementById('htmlOutput');
if (htmlOutput) htmlOutput.value = previewHtml;
// Preview tags
const previewTags = [
detectedArtist,
detectedTitle,
'vinyl',
'record',
vinylCond,
'lp',
year || 'vintage'
].filter(Boolean);
const tagsOutput = document.getElementById('tagsOutput');
if (tagsOutput) {
tagsOutput.innerHTML = previewTags.map(t => `
${t}
`).join('');
}
// Update shot list
renderShotList();
// Show results
const resultsSection = document.getElementById('resultsSection');
const emptyState = document.getElementById('emptyState');
if (resultsSection) resultsSection.classList.remove('hidden');
if (emptyState) emptyState.classList.add('hidden');
if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' });
showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success');
} catch (error) {
console.error('Preview error:', error);
showToast('Preview failed: ' + error.message, 'error');
} finally {
stopAnalysisProgress();
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
async function callAI(messages, temperature = 0.7) {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}`
},
body: JSON.stringify({
model: localStorage.getItem('deepseek_model') || 'deepseek-chat',
messages: messages,
temperature: temperature,
max_tokens: 2000
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'DeepSeek API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`DeepSeek Error: ${error.message}`, 'error');
return null;
}
} else {
// Fallback to OpenAI
const apiKey = localStorage.getItem('openai_api_key');
const model = localStorage.getItem('openai_model') || 'gpt-4o';
const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
if (!apiKey) {
showToast('OpenAI API key not configured. Go to Settings.', 'error');
return null;
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: messages,
temperature: temperature,
max_tokens: maxTokens
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`OpenAI Error: ${error.message}`, 'error');
return null;
}
}
}
// Legacy alias for backward compatibility
async function callOpenAI(messages, temperature = 0.7) {
return callAI(messages, temperature);
}
// Delete hosted image from imgBB
async function deleteHostedImage(deleteUrl) {
if (!deleteUrl) return false;
try {
const response = await fetch(deleteUrl, { method: 'GET' });
// imgBB delete URLs work via GET request
return response.ok;
} catch (error) {
console.error('Failed to delete image:', error);
return false;
}
}
// Get hosted photo URLs for eBay HTML description
function getHostedPhotoUrlsForEbay() {
return hostedPhotoUrls.map(img => ({
full: img.url,
display: img.displayUrl || img.url,
thumb: img.thumb,
medium: img.medium,
viewer: img.viewerUrl
}));
}
async function generateListingWithAI() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
if (!artist || !title) {
showToast('Please enter at least artist and title', 'error');
return;
}
const messages = [
{
role: 'system',
content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
},
{
role: 'user',
content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
}
];
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success');
const result = await callAI(messages, 0.7);
if (result) {
try {
const data = JSON.parse(result);
// Populate the UI with AI-generated content
if (data.titles) {
renderTitleOptions(data.titles.map(t => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: 'AI Generated'
})));
}
if (data.description) {
document.getElementById('htmlOutput').value = data.description;
}
if (data.tags) {
const tagsContainer = document.getElementById('tagsOutput');
tagsContainer.innerHTML = data.tags.map(t => `
${t}
`).join('');
}
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
showToast('AI listing generated!', 'success');
} catch (e) {
// If not valid JSON, treat as plain text description
document.getElementById('htmlOutput').value = result;
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
}
}
}
function requestHelp() {
alert(`VINYL PHOTO GUIDE:
ESSENTIAL SHOTS (need these):
• Front cover - square, no glare, color accurate
• Back cover - full frame, readable text
• Both labels - close enough to read all text
• Deadwax/runout - for pressing identification
CONDITION SHOTS:
• Vinyl in raking light at angle (shows scratches)
• Sleeve edges and corners
• Any flaws clearly documented
OPTIONARY BUT HELPFUL:
• Inner sleeve condition
• Inserts, posters, extras
• Hype stickers
• Barcode area
TIPS:
- Use natural daylight or 5500K bulbs
- Avoid flash directly on glossy sleeves
- Include scale reference if unusual size
- Photograph flaws honestly - reduces returns`);
}
function showToast(message, type = 'success') {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const iconMap = {
success: 'check',
error: 'alert-circle',
warning: 'alert-triangle'
};
const colorMap = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400'
};
const toast = document.createElement('div');
toast.className = `toast ${type} flex items-center gap-3`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
feather.replace();
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Cleanup function to delete all hosted images for current listing
async function cleanupHostedImages() {
if (window.currentListingImages) {
for (const img of window.currentListingImages) {
if (img.deleteUrl) {
await deleteHostedImage(img.deleteUrl);
}
}
window.currentListingImages = [];
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('VinylVault Pro initialized');
// Initialize drop zone
initDropZone();
// Warn about unsaved changes when leaving page with hosted images
window.addEventListener('beforeunload', (e) => {
if (hostedPhotoUrls.length > 0 && !window.listingPublished) {
// Optional: could add cleanup here or warn user
}
});
});
: '€';
// Mock comp research results
const comps = {
nm: { low: 45, high: 65, median: 52 },
vgplus: { low: 28, high: 42, median: 34 },
vg: { low: 15, high: 25, median: 19 }
};
// Calculate recommended price based on goal
let recommendedBin, strategy;
switch(goal) {
case 'quick':
recommendedBin = Math.round(comps.vgplus.low * 0.9);
strategy = 'BIN + Best Offer (aggressive)';
break;
case 'max':
recommendedBin = Math.round(comps.nm.high * 1.1);
strategy = 'BIN only, no offers, long duration';
break;
default:
recommendedBin = comps.vgplus.median;
strategy = 'BIN + Best Offer (standard)';
}
// Fee calculation (eBay UK approx)
const ebayFeeRate = 0.13; // 13% final value fee
const paypalRate = 0.029; // 2.9% + 30p
const fixedFee = 0.30;
const shippingCost = 4.50; // Estimated
const packingCost = 1.50;
const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee;
const breakEven = cost + totalFees + shippingCost + packingCost;
const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer
// Generate titles
const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`;
const titles = generateTitles(baseTitle, catNo, year, goal);
// Render results
renderTitleOptions(titles);
renderPricingStrategy(recommendedBin, strategy, comps, currency, goal);
renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency);
await renderHTMLDescription(data, titles[0]);
renderTags(artist, title, catNo, year);
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
currentAnalysis = {
titles, recommendedBin, strategy, breakEven, safeFloor, currency
};
}
function generateTitles(base, catNo, year, goal) {
const titles = [];
const cat = catNo || 'CAT#';
const yr = year || 'YEAR';
const country = window.detectedCountry || 'UK';
const genre = window.detectedGenre || 'Rock';
const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP';
// Option 1: Classic collector focus
titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`);
// Option 2: Condition forward
titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`);
// Option 3: Rarity/hype with detected genre
titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`);
// Option 4: Clean searchable
titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`);
// Option 5: Genre tagged
titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`);
return titles.map((t, i) => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i]
}));
}
function renderTitleOptions(titles) {
const container = document.getElementById('titleOptions');
container.innerHTML = titles.map((t, i) => `
${t.chars}/80
${t.text}
${t.style}
`).join('');
}
function selectTitle(el, text) {
document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
// Update clipboard copy
navigator.clipboard.writeText(text);
showToast('Title copied to clipboard!', 'success');
}
function renderPricingStrategy(bin, strategy, comps, currency, goal) {
const container = document.getElementById('pricingStrategy');
const offerSettings = goal === 'max' ? 'Offers: OFF' :
`Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`;
container.innerHTML = `
RECOMMENDED
${currency}${bin}
Buy It Now
Strategy: ${strategy}
Best Offer: ${offerSettings}
Duration: 30 days (GTC)
Sold Comps by Grade
NM/NM-
${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX
${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+
${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})
Based on last 90 days sold listings, same pressing. Prices exclude postage.
`;
}
function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
const container = document.getElementById('feeFloor');
container.innerHTML = `
Your Cost
${currency}${cost.toFixed(2)}
Est. Fees
${currency}${fees.toFixed(2)}
~16% total
Ship + Pack
${currency}${(shipping + packing).toFixed(2)}
Safe Floor Price
${currency}${safeFloor}
Auto-decline below this
`;
}
async function renderHTMLDescription(data, titleObj) {
const { artist, title, catNo, year } = data;
// Use hosted URL if available, otherwise fallback to local object URL
let heroImg = '';
let galleryImages = [];
if (hostedPhotoUrls.length > 0) {
heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url;
galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url);
} else if (uploadedPhotos.length > 0) {
heroImg = URL.createObjectURL(uploadedPhotos[0]);
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
}
// Use OCR-detected values if available
const detectedLabel = window.detectedLabel || '[Verify from photos]';
const detectedCountry = window.detectedCountry || 'UK';
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
const detectedGenre = window.detectedGenre || 'rock';
const detectedCondition = window.detectedCondition || 'VG+/VG+';
const detectedPressingInfo = window.detectedPressingInfo || '';
// Fetch tracklist and detailed info from Discogs if available
let tracklistHtml = '';
let pressingDetailsHtml = '';
let provenanceHtml = '';
if (window.discogsReleaseId && window.discogsService?.key) {
try {
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
if (discogsData && discogsData.tracklist) {
// Build tracklist HTML
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
if (hasSideBreakdown) {
// Group by sides
const sides = {};
discogsData.tracklist.forEach(track => {
const side = track.position ? track.position.charAt(0) : 'Other';
if (!sides[side]) sides[side] = [];
sides[side].push(track);
});
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
Side ${side}
${tracks.map(track => `
${track.position} ${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`).join('');
} else {
// Simple list
tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`;
}
// Build pressing/variation details
const identifiers = discogsData.identifiers || [];
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
pressingDetailsHtml = `
Pressing & Matrix Information
${barcodeInfo ? `
Barcode: ${barcodeInfo.value}
` : ''}
${matrixInfo.map(m => `
${m.type}: ${m.value}${m.description ? ` (${m.description}) ` : ''}
`).join('')}
${pressingInfo.map(p => `
${p.type}: ${p.value}
`).join('')}
${discogsData.notes ? `
${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}
` : ''}
`;
}
// Build provenance data for buyer confidence
const companies = discogsData.companies || [];
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
if (masteredBy || pressedBy || lacquerCut) {
provenanceHtml = `
Provenance & Production
${masteredBy ? `
✓ Mastered at ${masteredBy.name}
` : ''}
${lacquerCut ? `
✓ Lacquer cut at ${lacquerCut.name}
` : ''}
${pressedBy ? `
✓ Pressed at ${pressedBy.name}
` : ''}
${discogsData.num_for_sale ? `
Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs
` : ''}
`;
}
}
} catch (e) {
console.error('Failed to fetch Discogs details for HTML:', e);
}
}
// If no tracklist from Discogs, provide placeholder
if (!tracklistHtml) {
tracklistHtml = `Tracklist verification recommended. Please compare with Discogs entry for accuracy.
`;
}
const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `
`).join('')}
` : '';
const html = `
${galleryHtml}
Original ${detectedCountry} Pressing
${year || '1970s'}
${detectedFormat}
${detectedCondition}
Artist
${artist || 'See title'}
Title
${title || 'See title'}
Label
${detectedLabel}
Catalogue
${catNo || '[See photos]'}
Country
${detectedCountry}
Year
${year || '[Verify]'}
Condition Report
Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]
About This Release
${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]
Tracklist
${tracklistHtml}
${pressingDetailsHtml}
${provenanceHtml}
Packing & Postage
Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.
Combined postage: Discount available for multiple purchases—please request invoice before payment.
Questions? Need more photos?
Message me anytime—happy to provide additional angles, audio clips, or pressing details.
`;
// Store reference to hosted images for potential cleanup
window.currentListingImages = hostedPhotoUrls.map(img => ({
url: img.url,
deleteUrl: img.deleteUrl
}));
document.getElementById('htmlOutput').value = html;
}
function renderTags(artist, title, catNo, year) {
const genre = window.detectedGenre || 'rock';
const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' :
window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp';
const country = window.detectedCountry?.toLowerCase() || 'uk';
const tags = [
artist || 'vinyl',
title || 'record',
format,
'vinyl record',
'original pressing',
`${country} pressing`,
year || 'vintage',
catNo || '',
genre,
genre === 'rock' ? 'prog rock' : genre,
genre === 'rock' ? 'psych' : '',
'collector',
'audiophile',
format === 'lp' ? '12 inch' : format,
'33 rpm',
format === 'lp' ? 'album' : 'single',
'used vinyl',
'graded',
'excellent condition',
'rare vinyl',
'classic rock',
'vintage vinyl',
'record collection',
'music',
'audio',
window.detectedLabel || ''
].filter(Boolean);
const container = document.getElementById('tagsOutput');
container.innerHTML = tags.map(t => `
${t}
`).join('');
}
function renderShotList() {
// Map shot types to display info
const shotDefinitions = [
{ id: 'front', name: 'Front cover (square, well-lit)', critical: true },
{ id: 'back', name: 'Back cover (full shot)', critical: true },
{ id: 'spine', name: 'Spine (readable text)', critical: true },
{ id: 'label_a', name: 'Label Side A (close, legible)', critical: true },
{ id: 'label_b', name: 'Label Side B (close, legible)', critical: true },
{ id: 'deadwax', name: 'Deadwax/runout grooves', critical: true },
{ id: 'inner', name: 'Inner sleeve (both sides)', critical: false },
{ id: 'insert', name: 'Insert/poster if included', critical: false },
{ id: 'hype', name: 'Hype sticker (if present)', critical: false },
{ id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true },
{ id: 'corners', name: 'Sleeve corners/edges detail', critical: false },
{ id: 'barcode', name: 'Barcode area', critical: false }
];
// Check if we have any photos at all
const hasPhotos = uploadedPhotos.length > 0;
const container = document.getElementById('shotList');
container.innerHTML = shotDefinitions.map(shot => {
const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1);
const statusClass = have ? 'completed' : shot.critical ? 'missing' : '';
const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500';
const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300';
const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle';
return `
${shot.name}
${shot.critical && !have ? 'CRITICAL ' : ''}
`}).join('');
feather.replace();
}
function copyHTML() {
const html = document.getElementById('htmlOutput');
html.select();
document.execCommand('copy');
showToast('HTML copied to clipboard!', 'success');
}
function copyTags() {
const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', ');
navigator.clipboard.writeText(tags);
showToast('Tags copied to clipboard!', 'success');
}
async function draftAnalysis() {
if (uploadedPhotos.length === 0) {
showToast('Upload photos first for preview', 'error');
return;
}
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
// Show loading state
const dropZone = document.getElementById('dropZone');
const spinner = document.getElementById('uploadSpinner');
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
startAnalysisProgressSimulation();
try {
// Try OCR/AI analysis if available
const service = getAIService();
let ocrResult = null;
if (service && service.apiKey && uploadedPhotos.length > 0) {
try {
ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed
populateFieldsFromOCR(ocrResult);
} catch (e) {
console.log('Preview OCR failed:', e);
}
}
// Generate quick preview results
const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || '';
const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || '';
const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist';
const detectedTitle = title || ocrResult?.title || 'Unknown Title';
const baseTitle = `${detectedArtist} - ${detectedTitle}`;
// Generate quick titles
const quickTitles = [
`${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80),
`${baseTitle} Original Pressing Vinyl LP`.substring(0, 80),
`${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80)
].map((t, i) => ({
text: t,
chars: t.length,
style: ['Quick', 'Standard', 'Compact'][i]
}));
// Quick pricing estimate based on condition
const cost = parseFloat(document.getElementById('costInput').value) || 10;
const vinylCond = document.getElementById('vinylConditionInput').value;
const sleeveCond = document.getElementById('sleeveConditionInput').value;
const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 };
const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3;
const estimatedValue = Math.round(cost * Math.max(condMult, 1.5));
const suggestedPrice = Math.round(estimatedValue * 0.9);
// Render preview results
renderTitleOptions(quickTitles);
// Quick pricing card
document.getElementById('pricingStrategy').innerHTML = `
QUICK ESTIMATE
£${suggestedPrice}
Suggested Buy It Now
Est. Value: £${estimatedValue}
Your Cost: £${cost.toFixed(2)}
Condition: ${vinylCond}/${sleeveCond}
Preview Notes
${ocrResult ?
`
✓ AI detected information from photos
` :
`
⚠ Add API key in Settings for auto-detection
`
}
This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.
${ocrResult ? `
Detected from photos:
${ocrResult.artist ? `• Artist: ${ocrResult.artist} ` : ''}
${ocrResult.title ? `• Title: ${ocrResult.title} ` : ''}
${ocrResult.catalogueNumber ? `• Cat#: ${ocrResult.catalogueNumber} ` : ''}
${ocrResult.year ? `• Year: ${ocrResult.year} ` : ''}
` : ''}
`;
// Simple fee floor
const fees = suggestedPrice * 0.16;
const safeFloor = Math.ceil(cost + fees + 6);
document.getElementById('feeFloor').innerHTML = `
Your Cost
£${cost.toFixed(2)}
Est. Fees
£${fees.toFixed(2)}
`;
// Preview HTML description
const previewHtml = `
${detectedArtist} - ${detectedTitle}
${year ? `
Year: ${year}
` : ''}
${catNo ? `
Catalogue #: ${catNo}
` : ''}
Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}
[Full description will be generated with complete market analysis]
`;
document.getElementById('htmlOutput').value = previewHtml;
// Preview tags
const previewTags = [
detectedArtist,
detectedTitle,
'vinyl',
'record',
vinylCond,
'lp',
year || 'vintage'
].filter(Boolean);
document.getElementById('tagsOutput').innerHTML = previewTags.map(t => `
${t}
`).join('');
// Update shot list
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success');
} catch (error) {
console.error('Preview error:', error);
showToast('Preview failed: ' + error.message, 'error');
} finally {
stopAnalysisProgress();
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
async function callAI(messages, temperature = 0.7) {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}`
},
body: JSON.stringify({
model: localStorage.getItem('deepseek_model') || 'deepseek-chat',
messages: messages,
temperature: temperature,
max_tokens: 2000
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'DeepSeek API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`DeepSeek Error: ${error.message}`, 'error');
return null;
}
} else {
// Fallback to OpenAI
const apiKey = localStorage.getItem('openai_api_key');
const model = localStorage.getItem('openai_model') || 'gpt-4o';
const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
if (!apiKey) {
showToast('OpenAI API key not configured. Go to Settings.', 'error');
return null;
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: messages,
temperature: temperature,
max_tokens: maxTokens
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`OpenAI Error: ${error.message}`, 'error');
return null;
}
}
}
// Legacy alias for backward compatibility
async function callOpenAI(messages, temperature = 0.7) {
return callAI(messages, temperature);
}
// Delete hosted image from imgBB
async function deleteHostedImage(deleteUrl) {
if (!deleteUrl) return false;
try {
const response = await fetch(deleteUrl, { method: 'GET' });
// imgBB delete URLs work via GET request
return response.ok;
} catch (error) {
console.error('Failed to delete image:', error);
return false;
}
}
// Get hosted photo URLs for eBay HTML description
function getHostedPhotoUrlsForEbay() {
return hostedPhotoUrls.map(img => ({
full: img.url,
display: img.displayUrl || img.url,
thumb: img.thumb,
medium: img.medium,
viewer: img.viewerUrl
}));
}
async function generateListingWithAI() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
if (!artist || !title) {
showToast('Please enter at least artist and title', 'error');
return;
}
const messages = [
{
role: 'system',
content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
},
{
role: 'user',
content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
}
];
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success');
const result = await callAI(messages, 0.7);
if (result) {
try {
const data = JSON.parse(result);
// Populate the UI with AI-generated content
if (data.titles) {
renderTitleOptions(data.titles.map(t => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: 'AI Generated'
})));
}
if (data.description) {
document.getElementById('htmlOutput').value = data.description;
}
if (data.tags) {
const tagsContainer = document.getElementById('tagsOutput');
tagsContainer.innerHTML = data.tags.map(t => `
${t}
`).join('');
}
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
showToast('AI listing generated!', 'success');
} catch (e) {
// If not valid JSON, treat as plain text description
document.getElementById('htmlOutput').value = result;
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
}
}
}
function requestHelp() {
alert(`VINYL PHOTO GUIDE:
ESSENTIAL SHOTS (need these):
• Front cover - square, no glare, color accurate
• Back cover - full frame, readable text
• Both labels - close enough to read all text
• Deadwax/runout - for pressing identification
CONDITION SHOTS:
• Vinyl in raking light at angle (shows scratches)
• Sleeve edges and corners
• Any flaws clearly documented
OPTIONARY BUT HELPFUL:
• Inner sleeve condition
• Inserts, posters, extras
• Hype stickers
• Barcode area
TIPS:
- Use natural daylight or 5500K bulbs
- Avoid flash directly on glossy sleeves
- Include scale reference if unusual size
- Photograph flaws honestly - reduces returns`);
}
function showToast(message, type = 'success') {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const iconMap = {
success: 'check',
error: 'alert-circle',
warning: 'alert-triangle'
};
const colorMap = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400'
};
const toast = document.createElement('div');
toast.className = `toast ${type} flex items-center gap-3`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
feather.replace();
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Cleanup function to delete all hosted images for current listing
async function cleanupHostedImages() {
if (window.currentListingImages) {
for (const img of window.currentListingImages) {
if (img.deleteUrl) {
await deleteHostedImage(img.deleteUrl);
}
}
window.currentListingImages = [];
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('VinylVault Pro initialized');
// Initialize drop zone
initDropZone();
// Warn about unsaved changes when leaving page with hosted images
window.addEventListener('beforeunload', (e) => {
if (hostedPhotoUrls.length > 0 && !window.listingPublished) {
// Optional: could add cleanup here or warn user
}
});
});
: '€';
// Mock comp research results
const comps = {
nm: { low: 45, high: 65, median: 52 },
vgplus: { low: 28, high: 42, median: 34 },
vg: { low: 15, high: 25, median: 19 }
};
// Calculate recommended price based on goal
let recommendedBin, strategy;
switch(goal) {
case 'quick':
recommendedBin = Math.round(comps.vgplus.low * 0.9);
strategy = 'BIN + Best Offer (aggressive)';
break;
case 'max':
recommendedBin = Math.round(comps.nm.high * 1.1);
strategy = 'BIN only, no offers, long duration';
break;
default:
recommendedBin = comps.vgplus.median;
strategy = 'BIN + Best Offer (standard)';
}
// Fee calculation (eBay UK approx)
const ebayFeeRate = 0.13; // 13% final value fee
const paypalRate = 0.029; // 2.9% + 30p
const fixedFee = 0.30;
const shippingCost = 4.50; // Estimated
const packingCost = 1.50;
const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee;
const breakEven = cost + totalFees + shippingCost + packingCost;
const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer
// Generate titles
const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`;
const titles = generateTitles(baseTitle, catNo, year, goal);
// Render results
renderTitleOptions(titles);
renderPricingStrategy(recommendedBin, strategy, comps, currency, goal);
renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency);
await renderHTMLDescription(data, titles[0]);
renderTags(artist, title, catNo, year);
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
currentAnalysis = {
titles, recommendedBin, strategy, breakEven, safeFloor, currency
};
}
function renderTitleOptions(titles) {
const container = document.getElementById('titleOptions');
container.innerHTML = titles.map((t, i) => `
${t.chars}/80
${t.text}
${t.style}
`).join('');
}
function selectTitle(el, text) {
document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
// Update clipboard copy
navigator.clipboard.writeText(text);
showToast('Title copied to clipboard!', 'success');
}
function renderPricingStrategy(bin, strategy, comps, currency, goal) {
const container = document.getElementById('pricingStrategy');
const offerSettings = goal === 'max' ? 'Offers: OFF' :
`Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`;
container.innerHTML = `
RECOMMENDED
${currency}${bin}
Buy It Now
Strategy: ${strategy}
Best Offer: ${offerSettings}
Duration: 30 days (GTC)
Sold Comps by Grade
NM/NM-
${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX
${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+
${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})
Based on last 90 days sold listings, same pressing. Prices exclude postage.
`;
}
function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
const container = document.getElementById('feeFloor');
container.innerHTML = `
Your Cost
${currency}${cost.toFixed(2)}
Est. Fees
${currency}${fees.toFixed(2)}
~16% total
Ship + Pack
${currency}${(shipping + packing).toFixed(2)}
Safe Floor Price
${currency}${safeFloor}
Auto-decline below this
`;
}
async function renderHTMLDescription(data, titleObj) {
const { artist, title, catNo, year } = data;
// Use hosted URL if available, otherwise fallback to local object URL
let heroImg = '';
let galleryImages = [];
if (hostedPhotoUrls.length > 0) {
heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url;
galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url);
} else if (uploadedPhotos.length > 0) {
heroImg = URL.createObjectURL(uploadedPhotos[0]);
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
}
// Use OCR-detected values if available
const detectedLabel = window.detectedLabel || '[Verify from photos]';
const detectedCountry = window.detectedCountry || 'UK';
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
const detectedGenre = window.detectedGenre || 'rock';
const detectedCondition = window.detectedCondition || 'VG+/VG+';
const detectedPressingInfo = window.detectedPressingInfo || '';
// Fetch tracklist and detailed info from Discogs if available
let tracklistHtml = '';
let pressingDetailsHtml = '';
let provenanceHtml = '';
if (window.discogsReleaseId && window.discogsService?.key) {
try {
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
if (discogsData && discogsData.tracklist) {
// Build tracklist HTML
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
if (hasSideBreakdown) {
// Group by sides
const sides = {};
discogsData.tracklist.forEach(track => {
const side = track.position ? track.position.charAt(0) : 'Other';
if (!sides[side]) sides[side] = [];
sides[side].push(track);
});
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
Side ${side}
${tracks.map(track => `
${track.position} ${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`).join('');
} else {
// Simple list
tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`;
}
// Build pressing/variation details
const identifiers = discogsData.identifiers || [];
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
pressingDetailsHtml = `
Pressing & Matrix Information
${barcodeInfo ? `
Barcode: ${barcodeInfo.value}
` : ''}
${matrixInfo.map(m => `
${m.type}: ${m.value}${m.description ? ` (${m.description}) ` : ''}
`).join('')}
${pressingInfo.map(p => `
${p.type}: ${p.value}
`).join('')}
${discogsData.notes ? `
${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}
` : ''}
`;
}
// Build provenance data for buyer confidence
const companies = discogsData.companies || [];
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
if (masteredBy || pressedBy || lacquerCut) {
provenanceHtml = `
Provenance & Production
${masteredBy ? `
✓ Mastered at ${masteredBy.name}
` : ''}
${lacquerCut ? `
✓ Lacquer cut at ${lacquerCut.name}
` : ''}
${pressedBy ? `
✓ Pressed at ${pressedBy.name}
` : ''}
${discogsData.num_for_sale ? `
Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs
` : ''}
`;
}
}
} catch (e) {
console.error('Failed to fetch Discogs details for HTML:', e);
}
}
// If no tracklist from Discogs, provide placeholder
if (!tracklistHtml) {
tracklistHtml = `Tracklist verification recommended. Please compare with Discogs entry for accuracy.
`;
}
const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `
`).join('')}
` : '';
const html = `
${galleryHtml}
Original ${detectedCountry} Pressing
${year || '1970s'}
${detectedFormat}
${detectedCondition}
Artist
${artist || 'See title'}
Title
${title || 'See title'}
Label
${detectedLabel}
Catalogue
${catNo || '[See photos]'}
Country
${detectedCountry}
Year
${year || '[Verify]'}
Condition Report
Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]
About This Release
${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]
Tracklist
${tracklistHtml}
${pressingDetailsHtml}
${provenanceHtml}
Packing & Postage
Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.
Combined postage: Discount available for multiple purchases—please request invoice before payment.
Questions? Need more photos?
Message me anytime—happy to provide additional angles, audio clips, or pressing details.
`;
// Store reference to hosted images for potential cleanup
window.currentListingImages = hostedPhotoUrls.map(img => ({
url: img.url,
deleteUrl: img.deleteUrl
}));
document.getElementById('htmlOutput').value = html;
}
function renderTags(artist, title, catNo, year) {
const genre = window.detectedGenre || 'rock';
const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' :
window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp';
const country = window.detectedCountry?.toLowerCase() || 'uk';
const tags = [
artist || 'vinyl',
title || 'record',
format,
'vinyl record',
'original pressing',
`${country} pressing`,
year || 'vintage',
catNo || '',
genre,
genre === 'rock' ? 'prog rock' : genre,
genre === 'rock' ? 'psych' : '',
'collector',
'audiophile',
format === 'lp' ? '12 inch' : format,
'33 rpm',
format === 'lp' ? 'album' : 'single',
'used vinyl',
'graded',
'excellent condition',
'rare vinyl',
'classic rock',
'vintage vinyl',
'record collection',
'music',
'audio',
window.detectedLabel || ''
].filter(Boolean);
const container = document.getElementById('tagsOutput');
container.innerHTML = tags.map(t => `
${t}
`).join('');
}
function renderShotList() {
// Map shot types to display info
const shotDefinitions = [
{ id: 'front', name: 'Front cover (square, well-lit)', critical: true },
{ id: 'back', name: 'Back cover (full shot)', critical: true },
{ id: 'spine', name: 'Spine (readable text)', critical: true },
{ id: 'label_a', name: 'Label Side A (close, legible)', critical: true },
{ id: 'label_b', name: 'Label Side B (close, legible)', critical: true },
{ id: 'deadwax', name: 'Deadwax/runout grooves', critical: true },
{ id: 'inner', name: 'Inner sleeve (both sides)', critical: false },
{ id: 'insert', name: 'Insert/poster if included', critical: false },
{ id: 'hype', name: 'Hype sticker (if present)', critical: false },
{ id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true },
{ id: 'corners', name: 'Sleeve corners/edges detail', critical: false },
{ id: 'barcode', name: 'Barcode area', critical: false }
];
// Check if we have any photos at all
const hasPhotos = uploadedPhotos.length > 0;
const container = document.getElementById('shotList');
container.innerHTML = shotDefinitions.map(shot => {
const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1);
const statusClass = have ? 'completed' : shot.critical ? 'missing' : '';
const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500';
const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300';
const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle';
return `
${shot.name}
${shot.critical && !have ? 'CRITICAL ' : ''}
`}).join('');
feather.replace();
}
function copyHTML() {
const html = document.getElementById('htmlOutput');
html.select();
document.execCommand('copy');
showToast('HTML copied to clipboard!', 'success');
}
function copyTags() {
const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', ');
navigator.clipboard.writeText(tags);
showToast('Tags copied to clipboard!', 'success');
}
// Preview/Draft Analysis - quick analysis without full AI generation
async function draftAnalysis() {
if (uploadedPhotos.length === 0) {
showToast('Upload photos first for preview', 'error');
return;
}
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
// Show loading state
const dropZone = document.getElementById('dropZone');
const spinner = document.getElementById('uploadSpinner');
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
startAnalysisProgressSimulation();
try {
// Try OCR/AI analysis if available
const service = getAIService();
let ocrResult = null;
if (service && service.apiKey && uploadedPhotos.length > 0) {
try {
ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed
populateFieldsFromOCR(ocrResult);
} catch (e) {
console.log('Preview OCR failed:', e);
}
}
// Generate quick preview results
const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || '';
const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || '';
const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist';
const detectedTitle = title || ocrResult?.title || 'Unknown Title';
const baseTitle = `${detectedArtist} - ${detectedTitle}`;
// Generate quick titles
const quickTitles = [
`${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80),
`${baseTitle} Original Pressing Vinyl LP`.substring(0, 80),
`${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80)
].map((t, i) => ({
text: t,
chars: t.length,
style: ['Quick', 'Standard', 'Compact'][i]
}));
// Quick pricing estimate based on condition
const cost = parseFloat(document.getElementById('costInput').value) || 10;
const vinylCond = document.getElementById('vinylConditionInput').value;
const sleeveCond = document.getElementById('sleeveConditionInput').value;
const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 };
const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3;
const estimatedValue = Math.round(cost * Math.max(condMult, 1.5));
const suggestedPrice = Math.round(estimatedValue * 0.9);
// Render preview results
renderTitleOptions(quickTitles);
// Quick pricing card
document.getElementById('pricingStrategy').innerHTML = `
QUICK ESTIMATE
£${suggestedPrice}
Suggested Buy It Now
Est. Value: £${estimatedValue}
Your Cost: £${cost.toFixed(2)}
Condition: ${vinylCond}/${sleeveCond}
Preview Notes
${ocrResult ?
`
✓ AI detected information from photos
` :
`
⚠ Add API key in Settings for auto-detection
`
}
This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.
${ocrResult ? `
Detected from photos:
${ocrResult.artist ? `• Artist: ${ocrResult.artist} ` : ''}
${ocrResult.title ? `• Title: ${ocrResult.title} ` : ''}
${ocrResult.catalogueNumber ? `• Cat#: ${ocrResult.catalogueNumber} ` : ''}
${ocrResult.year ? `• Year: ${ocrResult.year} ` : ''}
` : ''}
`;
// Simple fee floor
const fees = suggestedPrice * 0.16;
const safeFloor = Math.ceil(cost + fees + 6);
document.getElementById('feeFloor').innerHTML = `
Your Cost
£${cost.toFixed(2)}
Est. Fees
£${fees.toFixed(2)}
`;
// Preview HTML description
const previewHtml = `
${detectedArtist} - ${detectedTitle}
${year ? `
Year: ${year}
` : ''}
${catNo ? `
Catalogue #: ${catNo}
` : ''}
Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}
[Full description will be generated with complete market analysis]
`;
const htmlOutput = document.getElementById('htmlOutput');
if (htmlOutput) htmlOutput.value = previewHtml;
// Preview tags
const previewTags = [
detectedArtist,
detectedTitle,
'vinyl',
'record',
vinylCond,
'lp',
year || 'vintage'
].filter(Boolean);
const tagsOutput = document.getElementById('tagsOutput');
if (tagsOutput) {
tagsOutput.innerHTML = previewTags.map(t => `
${t}
`).join('');
}
// Update shot list
renderShotList();
// Show results
const resultsSection = document.getElementById('resultsSection');
const emptyState = document.getElementById('emptyState');
if (resultsSection) resultsSection.classList.remove('hidden');
if (emptyState) emptyState.classList.add('hidden');
if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' });
showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success');
} catch (error) {
console.error('Preview error:', error);
showToast('Preview failed: ' + error.message, 'error');
} finally {
stopAnalysisProgress();
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
async function callAI(messages, temperature = 0.7) {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}`
},
body: JSON.stringify({
model: localStorage.getItem('deepseek_model') || 'deepseek-chat',
messages: messages,
temperature: temperature,
max_tokens: 2000
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'DeepSeek API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`DeepSeek Error: ${error.message}`, 'error');
return null;
}
} else {
// Fallback to OpenAI
const apiKey = localStorage.getItem('openai_api_key');
const model = localStorage.getItem('openai_model') || 'gpt-4o';
const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
if (!apiKey) {
showToast('OpenAI API key not configured. Go to Settings.', 'error');
return null;
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: messages,
temperature: temperature,
max_tokens: maxTokens
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`OpenAI Error: ${error.message}`, 'error');
return null;
}
}
}
// Legacy alias for backward compatibility
async function callOpenAI(messages, temperature = 0.7) {
return callAI(messages, temperature);
}
// Delete hosted image from imgBB
async function deleteHostedImage(deleteUrl) {
if (!deleteUrl) return false;
try {
const response = await fetch(deleteUrl, { method: 'GET' });
// imgBB delete URLs work via GET request
return response.ok;
} catch (error) {
console.error('Failed to delete image:', error);
return false;
}
}
// Get hosted photo URLs for eBay HTML description
function getHostedPhotoUrlsForEbay() {
return hostedPhotoUrls.map(img => ({
full: img.url,
display: img.displayUrl || img.url,
thumb: img.thumb,
medium: img.medium,
viewer: img.viewerUrl
}));
}
async function generateListingWithAI() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
if (!artist || !title) {
showToast('Please enter at least artist and title', 'error');
return;
}
const messages = [
{
role: 'system',
content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
},
{
role: 'user',
content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
}
];
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success');
const result = await callAI(messages, 0.7);
if (result) {
try {
const data = JSON.parse(result);
// Populate the UI with AI-generated content
if (data.titles) {
renderTitleOptions(data.titles.map(t => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: 'AI Generated'
})));
}
if (data.description) {
document.getElementById('htmlOutput').value = data.description;
}
if (data.tags) {
const tagsContainer = document.getElementById('tagsOutput');
tagsContainer.innerHTML = data.tags.map(t => `
${t}
`).join('');
}
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
showToast('AI listing generated!', 'success');
} catch (e) {
// If not valid JSON, treat as plain text description
document.getElementById('htmlOutput').value = result;
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
}
}
}
function requestHelp() {
alert(`VINYL PHOTO GUIDE:
ESSENTIAL SHOTS (need these):
• Front cover - square, no glare, color accurate
• Back cover - full frame, readable text
• Both labels - close enough to read all text
• Deadwax/runout - for pressing identification
CONDITION SHOTS:
• Vinyl in raking light at angle (shows scratches)
• Sleeve edges and corners
• Any flaws clearly documented
OPTIONARY BUT HELPFUL:
• Inner sleeve condition
• Inserts, posters, extras
• Hype stickers
• Barcode area
TIPS:
- Use natural daylight or 5500K bulbs
- Avoid flash directly on glossy sleeves
- Include scale reference if unusual size
- Photograph flaws honestly - reduces returns`);
}
function showToast(message, type = 'success') {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const iconMap = {
success: 'check',
error: 'alert-circle',
warning: 'alert-triangle'
};
const colorMap = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400'
};
const toast = document.createElement('div');
toast.className = `toast ${type} flex items-center gap-3`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
feather.replace();
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Cleanup function to delete all hosted images for current listing
async function cleanupHostedImages() {
if (window.currentListingImages) {
for (const img of window.currentListingImages) {
if (img.deleteUrl) {
await deleteHostedImage(img.deleteUrl);
}
}
window.currentListingImages = [];
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('VinylVault Pro initialized');
// Initialize drop zone
initDropZone();
// Attach event listeners to buttons
const generateBtn = document.getElementById('generateListingBtn');
if (generateBtn) {
generateBtn.addEventListener('click', generateListing);
}
const draftBtn = document.getElementById('draftAnalysisBtn');
if (draftBtn) {
draftBtn.addEventListener('click', draftAnalysis);
}
const helpBtn = document.getElementById('requestHelpBtn');
if (helpBtn) {
helpBtn.addEventListener('click', requestHelp);
}
const copyHTMLBtn = document.getElementById('copyHTMLBtn');
if (copyHTMLBtn) {
copyHTMLBtn.addEventListener('click', copyHTML);
}
const copyTagsBtn = document.getElementById('copyTagsBtn');
if (copyTagsBtn) {
copyTagsBtn.addEventListener('click', copyTags);
}
const analyzePhotoBtn = document.getElementById('analyzePhotoTypesBtn');
if (analyzePhotoBtn) {
analyzePhotoBtn.addEventListener('click', analyzePhotoTypes);
}
// Clear Collection Import Banner listeners
const clearCollectionBtn = document.querySelector('#collectionBanner button');
if (clearCollectionBtn) {
clearCollectionBtn.addEventListener('click', clearCollectionImport);
}
// Warn about unsaved changes when leaving page with hosted images
window.addEventListener('beforeunload', (e) => {
if (hostedPhotoUrls.length > 0 && !window.listingPublished) {
// Optional: could add cleanup here or warn user
}
});
});
// Collection Import functions (defined here to avoid reference errors)
function clearCollectionImport() {
sessionStorage.removeItem('collectionListingRecord');
const banner = document.getElementById('collectionBanner');
if (banner) {
banner.classList.add('hidden');
}
showToast('Collection import cleared', 'success');
}
function checkCollectionImport() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('fromCollection') === 'true') {
const recordData = sessionStorage.getItem('collectionListingRecord');
if (recordData) {
const record = JSON.parse(recordData);
populateFieldsFromCollection(record);
const banner = document.getElementById('collectionBanner');
if (banner) {
banner.classList.remove('hidden');
}
const indicator = document.getElementById('collectionDataIndicator');
if (indicator) {
indicator.classList.remove('hidden');
}
}
}
}
function populateFieldsFromCollection(record) {
if (!record) return;
const fields = {
'artistInput': record.artist,
'titleInput': record.title,
'catInput': record.catalogueNumber || record.matrixNotes,
'yearInput': record.year,
'costInput': record.purchasePrice,
'daysOwnedInput': record.daysOwned
};
Object.entries(fields).forEach(([fieldId, value]) => {
const field = document.getElementById(fieldId);
if (field && value) {
field.value = value;
}
});
// Set conditions if available
if (record.conditionVinyl) {
const vinylCondition = document.getElementById('vinylConditionInput');
if (vinylCondition) vinylCondition.value = record.conditionVinyl;
}
if (record.conditionSleeve) {
const sleeveCondition = document.getElementById('sleeveConditionInput');
if (sleeveCondition) sleeveCondition.value = record.conditionSleeve;
}
showToast(`Loaded ${record.artist} - ${record.title} from collection`, 'success');
}
// Call check on load
checkCollectionImport();
: '€';
// Mock comp research results
const comps = {
nm: { low: 45, high: 65, median: 52 },
vgplus: { low: 28, high: 42, median: 34 },
vg: { low: 15, high: 25, median: 19 }
};
// Calculate recommended price based on goal
let recommendedBin, strategy;
switch(goal) {
case 'quick':
recommendedBin = Math.round(comps.vgplus.low * 0.9);
strategy = 'BIN + Best Offer (aggressive)';
break;
case 'max':
recommendedBin = Math.round(comps.nm.high * 1.1);
strategy = 'BIN only, no offers, long duration';
break;
default:
recommendedBin = comps.vgplus.median;
strategy = 'BIN + Best Offer (standard)';
}
// Fee calculation (eBay UK approx)
const ebayFeeRate = 0.13; // 13% final value fee
const paypalRate = 0.029; // 2.9% + 30p
const fixedFee = 0.30;
const shippingCost = 4.50; // Estimated
const packingCost = 1.50;
const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee;
const breakEven = cost + totalFees + shippingCost + packingCost;
const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer
// Generate titles
const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`;
const titles = generateTitles(baseTitle, catNo, year, goal);
// Render results
renderTitleOptions(titles);
renderPricingStrategy(recommendedBin, strategy, comps, currency, goal);
renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency);
await renderHTMLDescription(data, titles[0]);
renderTags(artist, title, catNo, year);
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
currentAnalysis = {
titles, recommendedBin, strategy, breakEven, safeFloor, currency
};
}
function generateTitles(base, catNo, year, goal) {
const titles = [];
const cat = catNo || 'CAT#';
const yr = year || 'YEAR';
const country = window.detectedCountry || 'UK';
const genre = window.detectedGenre || 'Rock';
const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP';
// Option 1: Classic collector focus
titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`);
// Option 2: Condition forward
titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`);
// Option 3: Rarity/hype with detected genre
titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`);
// Option 4: Clean searchable
titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`);
// Option 5: Genre tagged
titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`);
return titles.map((t, i) => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i]
}));
}
function renderTitleOptions(titles) {
const container = document.getElementById('titleOptions');
container.innerHTML = titles.map((t, i) => `
${t.chars}/80
${t.text}
${t.style}
`).join('');
}
function selectTitle(el, text) {
document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
// Update clipboard copy
navigator.clipboard.writeText(text);
showToast('Title copied to clipboard!', 'success');
}
function renderPricingStrategy(bin, strategy, comps, currency, goal) {
const container = document.getElementById('pricingStrategy');
const offerSettings = goal === 'max' ? 'Offers: OFF' :
`Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`;
container.innerHTML = `
RECOMMENDED
${currency}${bin}
Buy It Now
Strategy: ${strategy}
Best Offer: ${offerSettings}
Duration: 30 days (GTC)
Sold Comps by Grade
NM/NM-
${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX
${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+
${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})
Based on last 90 days sold listings, same pressing. Prices exclude postage.
`;
}
function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
const container = document.getElementById('feeFloor');
container.innerHTML = `
Your Cost
${currency}${cost.toFixed(2)}
Est. Fees
${currency}${fees.toFixed(2)}
~16% total
Ship + Pack
${currency}${(shipping + packing).toFixed(2)}
Safe Floor Price
${currency}${safeFloor}
Auto-decline below this
`;
}
async function renderHTMLDescription(data, titleObj) {
const { artist, title, catNo, year } = data;
// Use hosted URL if available, otherwise fallback to local object URL
let heroImg = '';
let galleryImages = [];
if (hostedPhotoUrls.length > 0) {
heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url;
galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url);
} else if (uploadedPhotos.length > 0) {
heroImg = URL.createObjectURL(uploadedPhotos[0]);
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
}
// Use OCR-detected values if available
const detectedLabel = window.detectedLabel || '[Verify from photos]';
const detectedCountry = window.detectedCountry || 'UK';
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
const detectedGenre = window.detectedGenre || 'rock';
const detectedCondition = window.detectedCondition || 'VG+/VG+';
const detectedPressingInfo = window.detectedPressingInfo || '';
// Fetch tracklist and detailed info from Discogs if available
let tracklistHtml = '';
let pressingDetailsHtml = '';
let provenanceHtml = '';
if (window.discogsReleaseId && window.discogsService?.key) {
try {
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
if (discogsData && discogsData.tracklist) {
// Build tracklist HTML
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
if (hasSideBreakdown) {
// Group by sides
const sides = {};
discogsData.tracklist.forEach(track => {
const side = track.position ? track.position.charAt(0) : 'Other';
if (!sides[side]) sides[side] = [];
sides[side].push(track);
});
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
Side ${side}
${tracks.map(track => `
${track.position} ${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`).join('');
} else {
// Simple list
tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`;
}
// Build pressing/variation details
const identifiers = discogsData.identifiers || [];
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
pressingDetailsHtml = `
Pressing & Matrix Information
${barcodeInfo ? `
Barcode: ${barcodeInfo.value}
` : ''}
${matrixInfo.map(m => `
${m.type}: ${m.value}${m.description ? ` (${m.description}) ` : ''}
`).join('')}
${pressingInfo.map(p => `
${p.type}: ${p.value}
`).join('')}
${discogsData.notes ? `
${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}
` : ''}
`;
}
// Build provenance data for buyer confidence
const companies = discogsData.companies || [];
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
if (masteredBy || pressedBy || lacquerCut) {
provenanceHtml = `
Provenance & Production
${masteredBy ? `
✓ Mastered at ${masteredBy.name}
` : ''}
${lacquerCut ? `
✓ Lacquer cut at ${lacquerCut.name}
` : ''}
${pressedBy ? `
✓ Pressed at ${pressedBy.name}
` : ''}
${discogsData.num_for_sale ? `
Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs
` : ''}
`;
}
}
} catch (e) {
console.error('Failed to fetch Discogs details for HTML:', e);
}
}
// If no tracklist from Discogs, provide placeholder
if (!tracklistHtml) {
tracklistHtml = `Tracklist verification recommended. Please compare with Discogs entry for accuracy.
`;
}
const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `
`).join('')}
` : '';
const html = `
${galleryHtml}
Original ${detectedCountry} Pressing
${year || '1970s'}
${detectedFormat}
${detectedCondition}
Artist
${artist || 'See title'}
Title
${title || 'See title'}
Label
${detectedLabel}
Catalogue
${catNo || '[See photos]'}
Country
${detectedCountry}
Year
${year || '[Verify]'}
Condition Report
Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]
About This Release
${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]
Tracklist
${tracklistHtml}
${pressingDetailsHtml}
${provenanceHtml}
Packing & Postage
Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.
Combined postage: Discount available for multiple purchases—please request invoice before payment.
Questions? Need more photos?
Message me anytime—happy to provide additional angles, audio clips, or pressing details.
`;
// Store reference to hosted images for potential cleanup
window.currentListingImages = hostedPhotoUrls.map(img => ({
url: img.url,
deleteUrl: img.deleteUrl
}));
document.getElementById('htmlOutput').value = html;
}
function renderTags(artist, title, catNo, year) {
const genre = window.detectedGenre || 'rock';
const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' :
window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp';
const country = window.detectedCountry?.toLowerCase() || 'uk';
const tags = [
artist || 'vinyl',
title || 'record',
format,
'vinyl record',
'original pressing',
`${country} pressing`,
year || 'vintage',
catNo || '',
genre,
genre === 'rock' ? 'prog rock' : genre,
genre === 'rock' ? 'psych' : '',
'collector',
'audiophile',
format === 'lp' ? '12 inch' : format,
'33 rpm',
format === 'lp' ? 'album' : 'single',
'used vinyl',
'graded',
'excellent condition',
'rare vinyl',
'classic rock',
'vintage vinyl',
'record collection',
'music',
'audio',
window.detectedLabel || ''
].filter(Boolean);
const container = document.getElementById('tagsOutput');
container.innerHTML = tags.map(t => `
${t}
`).join('');
}
function renderShotList() {
// Map shot types to display info
const shotDefinitions = [
{ id: 'front', name: 'Front cover (square, well-lit)', critical: true },
{ id: 'back', name: 'Back cover (full shot)', critical: true },
{ id: 'spine', name: 'Spine (readable text)', critical: true },
{ id: 'label_a', name: 'Label Side A (close, legible)', critical: true },
{ id: 'label_b', name: 'Label Side B (close, legible)', critical: true },
{ id: 'deadwax', name: 'Deadwax/runout grooves', critical: true },
{ id: 'inner', name: 'Inner sleeve (both sides)', critical: false },
{ id: 'insert', name: 'Insert/poster if included', critical: false },
{ id: 'hype', name: 'Hype sticker (if present)', critical: false },
{ id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true },
{ id: 'corners', name: 'Sleeve corners/edges detail', critical: false },
{ id: 'barcode', name: 'Barcode area', critical: false }
];
// Check if we have any photos at all
const hasPhotos = uploadedPhotos.length > 0;
const container = document.getElementById('shotList');
container.innerHTML = shotDefinitions.map(shot => {
const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1);
const statusClass = have ? 'completed' : shot.critical ? 'missing' : '';
const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500';
const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300';
const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle';
return `
${shot.name}
${shot.critical && !have ? 'CRITICAL ' : ''}
`}).join('');
feather.replace();
}
function copyHTML() {
const html = document.getElementById('htmlOutput');
html.select();
document.execCommand('copy');
showToast('HTML copied to clipboard!', 'success');
}
function copyTags() {
const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', ');
navigator.clipboard.writeText(tags);
showToast('Tags copied to clipboard!', 'success');
}
async function draftAnalysis() {
if (uploadedPhotos.length === 0) {
showToast('Upload photos first for preview', 'error');
return;
}
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
// Show loading state
const dropZone = document.getElementById('dropZone');
const spinner = document.getElementById('uploadSpinner');
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
startAnalysisProgressSimulation();
try {
// Try OCR/AI analysis if available
const service = getAIService();
let ocrResult = null;
if (service && service.apiKey && uploadedPhotos.length > 0) {
try {
ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed
populateFieldsFromOCR(ocrResult);
} catch (e) {
console.log('Preview OCR failed:', e);
}
}
// Generate quick preview results
const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || '';
const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || '';
const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist';
const detectedTitle = title || ocrResult?.title || 'Unknown Title';
const baseTitle = `${detectedArtist} - ${detectedTitle}`;
// Generate quick titles
const quickTitles = [
`${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80),
`${baseTitle} Original Pressing Vinyl LP`.substring(0, 80),
`${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80)
].map((t, i) => ({
text: t,
chars: t.length,
style: ['Quick', 'Standard', 'Compact'][i]
}));
// Quick pricing estimate based on condition
const cost = parseFloat(document.getElementById('costInput').value) || 10;
const vinylCond = document.getElementById('vinylConditionInput').value;
const sleeveCond = document.getElementById('sleeveConditionInput').value;
const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 };
const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3;
const estimatedValue = Math.round(cost * Math.max(condMult, 1.5));
const suggestedPrice = Math.round(estimatedValue * 0.9);
// Render preview results
renderTitleOptions(quickTitles);
// Quick pricing card
document.getElementById('pricingStrategy').innerHTML = `
QUICK ESTIMATE
£${suggestedPrice}
Suggested Buy It Now
Est. Value: £${estimatedValue}
Your Cost: £${cost.toFixed(2)}
Condition: ${vinylCond}/${sleeveCond}
Preview Notes
${ocrResult ?
`
✓ AI detected information from photos
` :
`
⚠ Add API key in Settings for auto-detection
`
}
This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.
${ocrResult ? `
Detected from photos:
${ocrResult.artist ? `• Artist: ${ocrResult.artist} ` : ''}
${ocrResult.title ? `• Title: ${ocrResult.title} ` : ''}
${ocrResult.catalogueNumber ? `• Cat#: ${ocrResult.catalogueNumber} ` : ''}
${ocrResult.year ? `• Year: ${ocrResult.year} ` : ''}
` : ''}
`;
// Simple fee floor
const fees = suggestedPrice * 0.16;
const safeFloor = Math.ceil(cost + fees + 6);
document.getElementById('feeFloor').innerHTML = `
Your Cost
£${cost.toFixed(2)}
Est. Fees
£${fees.toFixed(2)}
`;
// Preview HTML description
const previewHtml = `
${detectedArtist} - ${detectedTitle}
${year ? `
Year: ${year}
` : ''}
${catNo ? `
Catalogue #: ${catNo}
` : ''}
Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}
[Full description will be generated with complete market analysis]
`;
document.getElementById('htmlOutput').value = previewHtml;
// Preview tags
const previewTags = [
detectedArtist,
detectedTitle,
'vinyl',
'record',
vinylCond,
'lp',
year || 'vintage'
].filter(Boolean);
document.getElementById('tagsOutput').innerHTML = previewTags.map(t => `
${t}
`).join('');
// Update shot list
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success');
} catch (error) {
console.error('Preview error:', error);
showToast('Preview failed: ' + error.message, 'error');
} finally {
stopAnalysisProgress();
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
async function callAI(messages, temperature = 0.7) {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}`
},
body: JSON.stringify({
model: localStorage.getItem('deepseek_model') || 'deepseek-chat',
messages: messages,
temperature: temperature,
max_tokens: 2000
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'DeepSeek API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`DeepSeek Error: ${error.message}`, 'error');
return null;
}
} else {
// Fallback to OpenAI
const apiKey = localStorage.getItem('openai_api_key');
const model = localStorage.getItem('openai_model') || 'gpt-4o';
const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
if (!apiKey) {
showToast('OpenAI API key not configured. Go to Settings.', 'error');
return null;
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: messages,
temperature: temperature,
max_tokens: maxTokens
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`OpenAI Error: ${error.message}`, 'error');
return null;
}
}
}
// Legacy alias for backward compatibility
async function callOpenAI(messages, temperature = 0.7) {
return callAI(messages, temperature);
}
// Delete hosted image from imgBB
async function deleteHostedImage(deleteUrl) {
if (!deleteUrl) return false;
try {
const response = await fetch(deleteUrl, { method: 'GET' });
// imgBB delete URLs work via GET request
return response.ok;
} catch (error) {
console.error('Failed to delete image:', error);
return false;
}
}
// Get hosted photo URLs for eBay HTML description
function getHostedPhotoUrlsForEbay() {
return hostedPhotoUrls.map(img => ({
full: img.url,
display: img.displayUrl || img.url,
thumb: img.thumb,
medium: img.medium,
viewer: img.viewerUrl
}));
}
async function generateListingWithAI() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
if (!artist || !title) {
showToast('Please enter at least artist and title', 'error');
return;
}
const messages = [
{
role: 'system',
content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
},
{
role: 'user',
content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
}
];
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success');
const result = await callAI(messages, 0.7);
if (result) {
try {
const data = JSON.parse(result);
// Populate the UI with AI-generated content
if (data.titles) {
renderTitleOptions(data.titles.map(t => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: 'AI Generated'
})));
}
if (data.description) {
document.getElementById('htmlOutput').value = data.description;
}
if (data.tags) {
const tagsContainer = document.getElementById('tagsOutput');
tagsContainer.innerHTML = data.tags.map(t => `
${t}
`).join('');
}
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
showToast('AI listing generated!', 'success');
} catch (e) {
// If not valid JSON, treat as plain text description
document.getElementById('htmlOutput').value = result;
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
}
}
}
function requestHelp() {
alert(`VINYL PHOTO GUIDE:
ESSENTIAL SHOTS (need these):
• Front cover - square, no glare, color accurate
• Back cover - full frame, readable text
• Both labels - close enough to read all text
• Deadwax/runout - for pressing identification
CONDITION SHOTS:
• Vinyl in raking light at angle (shows scratches)
• Sleeve edges and corners
• Any flaws clearly documented
OPTIONARY BUT HELPFUL:
• Inner sleeve condition
• Inserts, posters, extras
• Hype stickers
• Barcode area
TIPS:
- Use natural daylight or 5500K bulbs
- Avoid flash directly on glossy sleeves
- Include scale reference if unusual size
- Photograph flaws honestly - reduces returns`);
}
function showToast(message, type = 'success') {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const iconMap = {
success: 'check',
error: 'alert-circle',
warning: 'alert-triangle'
};
const colorMap = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400'
};
const toast = document.createElement('div');
toast.className = `toast ${type} flex items-center gap-3`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
feather.replace();
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Cleanup function to delete all hosted images for current listing
async function cleanupHostedImages() {
if (window.currentListingImages) {
for (const img of window.currentListingImages) {
if (img.deleteUrl) {
await deleteHostedImage(img.deleteUrl);
}
}
window.currentListingImages = [];
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('VinylVault Pro initialized');
// Initialize drop zone
initDropZone();
// Warn about unsaved changes when leaving page with hosted images
window.addEventListener('beforeunload', (e) => {
if (hostedPhotoUrls.length > 0 && !window.listingPublished) {
// Optional: could add cleanup here or warn user
}
});
});
: '€';
// Mock comp research results
const comps = {
nm: { low: 45, high: 65, median: 52 },
vgplus: { low: 28, high: 42, median: 34 },
vg: { low: 15, high: 25, median: 19 }
};
// Calculate recommended price based on goal
let recommendedBin, strategy;
switch(goal) {
case 'quick':
recommendedBin = Math.round(comps.vgplus.low * 0.9);
strategy = 'BIN + Best Offer (aggressive)';
break;
case 'max':
recommendedBin = Math.round(comps.nm.high * 1.1);
strategy = 'BIN only, no offers, long duration';
break;
default:
recommendedBin = comps.vgplus.median;
strategy = 'BIN + Best Offer (standard)';
}
// Fee calculation (eBay UK approx)
const ebayFeeRate = 0.13; // 13% final value fee
const paypalRate = 0.029; // 2.9% + 30p
const fixedFee = 0.30;
const shippingCost = 4.50; // Estimated
const packingCost = 1.50;
const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee;
const breakEven = cost + totalFees + shippingCost + packingCost;
const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer
// Generate titles
const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`;
const titles = generateTitles(baseTitle, catNo, year, goal);
// Render results
renderTitleOptions(titles);
renderPricingStrategy(recommendedBin, strategy, comps, currency, goal);
renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency);
await renderHTMLDescription(data, titles[0]);
renderTags(artist, title, catNo, year);
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
currentAnalysis = {
titles, recommendedBin, strategy, breakEven, safeFloor, currency
};
}
function generateTitles(base, catNo, year, goal) {
const titles = [];
const cat = catNo || 'CAT#';
const yr = year || 'YEAR';
const country = window.detectedCountry || 'UK';
const genre = window.detectedGenre || 'Rock';
const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP';
// Option 1: Classic collector focus
titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`);
// Option 2: Condition forward
titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`);
// Option 3: Rarity/hype with detected genre
titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`);
// Option 4: Clean searchable
titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`);
// Option 5: Genre tagged
titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`);
return titles.map((t, i) => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i]
}));
}
function renderTitleOptions(titles) {
const container = document.getElementById('titleOptions');
container.innerHTML = titles.map((t, i) => `
${t.chars}/80
${t.text}
${t.style}
`).join('');
}
function selectTitle(el, text) {
document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
// Update clipboard copy
navigator.clipboard.writeText(text);
showToast('Title copied to clipboard!', 'success');
}
function renderPricingStrategy(bin, strategy, comps, currency, goal) {
const container = document.getElementById('pricingStrategy');
const offerSettings = goal === 'max' ? 'Offers: OFF' :
`Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`;
container.innerHTML = `
RECOMMENDED
${currency}${bin}
Buy It Now
Strategy: ${strategy}
Best Offer: ${offerSettings}
Duration: 30 days (GTC)
Sold Comps by Grade
NM/NM-
${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX
${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+
${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})
Based on last 90 days sold listings, same pressing. Prices exclude postage.
`;
}
function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
const container = document.getElementById('feeFloor');
container.innerHTML = `
Your Cost
${currency}${cost.toFixed(2)}
Est. Fees
${currency}${fees.toFixed(2)}
~16% total
Ship + Pack
${currency}${(shipping + packing).toFixed(2)}
Safe Floor Price
${currency}${safeFloor}
Auto-decline below this
`;
}
async function renderHTMLDescription(data, titleObj) {
const { artist, title, catNo, year } = data;
// Use hosted URL if available, otherwise fallback to local object URL
let heroImg = '';
let galleryImages = [];
if (hostedPhotoUrls.length > 0) {
heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url;
galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url);
} else if (uploadedPhotos.length > 0) {
heroImg = URL.createObjectURL(uploadedPhotos[0]);
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
}
// Use OCR-detected values if available
const detectedLabel = window.detectedLabel || '[Verify from photos]';
const detectedCountry = window.detectedCountry || 'UK';
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
const detectedGenre = window.detectedGenre || 'rock';
const detectedCondition = window.detectedCondition || 'VG+/VG+';
const detectedPressingInfo = window.detectedPressingInfo || '';
// Fetch tracklist and detailed info from Discogs if available
let tracklistHtml = '';
let pressingDetailsHtml = '';
let provenanceHtml = '';
if (window.discogsReleaseId && window.discogsService?.key) {
try {
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
if (discogsData && discogsData.tracklist) {
// Build tracklist HTML
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
if (hasSideBreakdown) {
// Group by sides
const sides = {};
discogsData.tracklist.forEach(track => {
const side = track.position ? track.position.charAt(0) : 'Other';
if (!sides[side]) sides[side] = [];
sides[side].push(track);
});
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
Side ${side}
${tracks.map(track => `
${track.position} ${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`).join('');
} else {
// Simple list
tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`;
}
// Build pressing/variation details
const identifiers = discogsData.identifiers || [];
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
pressingDetailsHtml = `
Pressing & Matrix Information
${barcodeInfo ? `
Barcode: ${barcodeInfo.value}
` : ''}
${matrixInfo.map(m => `
${m.type}: ${m.value}${m.description ? ` (${m.description}) ` : ''}
`).join('')}
${pressingInfo.map(p => `
${p.type}: ${p.value}
`).join('')}
${discogsData.notes ? `
${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}
` : ''}
`;
}
// Build provenance data for buyer confidence
const companies = discogsData.companies || [];
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
if (masteredBy || pressedBy || lacquerCut) {
provenanceHtml = `
Provenance & Production
${masteredBy ? `
✓ Mastered at ${masteredBy.name}
` : ''}
${lacquerCut ? `
✓ Lacquer cut at ${lacquerCut.name}
` : ''}
${pressedBy ? `
✓ Pressed at ${pressedBy.name}
` : ''}
${discogsData.num_for_sale ? `
Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs
` : ''}
`;
}
}
} catch (e) {
console.error('Failed to fetch Discogs details for HTML:', e);
}
}
// If no tracklist from Discogs, provide placeholder
if (!tracklistHtml) {
tracklistHtml = `Tracklist verification recommended. Please compare with Discogs entry for accuracy.
`;
}
const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `
`).join('')}
` : '';
const html = `
${galleryHtml}
Original ${detectedCountry} Pressing
${year || '1970s'}
${detectedFormat}
${detectedCondition}
Artist
${artist || 'See title'}
Title
${title || 'See title'}
Label
${detectedLabel}
Catalogue
${catNo || '[See photos]'}
Country
${detectedCountry}
Year
${year || '[Verify]'}
Condition Report
Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]
About This Release
${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]
Tracklist
${tracklistHtml}
${pressingDetailsHtml}
${provenanceHtml}
Packing & Postage
Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.
Combined postage: Discount available for multiple purchases—please request invoice before payment.
Questions? Need more photos?
Message me anytime—happy to provide additional angles, audio clips, or pressing details.
`;
// Store reference to hosted images for potential cleanup
window.currentListingImages = hostedPhotoUrls.map(img => ({
url: img.url,
deleteUrl: img.deleteUrl
}));
document.getElementById('htmlOutput').value = html;
}
function renderTags(artist, title, catNo, year) {
const genre = window.detectedGenre || 'rock';
const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' :
window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp';
const country = window.detectedCountry?.toLowerCase() || 'uk';
const tags = [
artist || 'vinyl',
title || 'record',
format,
'vinyl record',
'original pressing',
`${country} pressing`,
year || 'vintage',
catNo || '',
genre,
genre === 'rock' ? 'prog rock' : genre,
genre === 'rock' ? 'psych' : '',
'collector',
'audiophile',
format === 'lp' ? '12 inch' : format,
'33 rpm',
format === 'lp' ? 'album' : 'single',
'used vinyl',
'graded',
'excellent condition',
'rare vinyl',
'classic rock',
'vintage vinyl',
'record collection',
'music',
'audio',
window.detectedLabel || ''
].filter(Boolean);
const container = document.getElementById('tagsOutput');
container.innerHTML = tags.map(t => `
${t}
`).join('');
}
function renderShotList() {
// Map shot types to display info
const shotDefinitions = [
{ id: 'front', name: 'Front cover (square, well-lit)', critical: true },
{ id: 'back', name: 'Back cover (full shot)', critical: true },
{ id: 'spine', name: 'Spine (readable text)', critical: true },
{ id: 'label_a', name: 'Label Side A (close, legible)', critical: true },
{ id: 'label_b', name: 'Label Side B (close, legible)', critical: true },
{ id: 'deadwax', name: 'Deadwax/runout grooves', critical: true },
{ id: 'inner', name: 'Inner sleeve (both sides)', critical: false },
{ id: 'insert', name: 'Insert/poster if included', critical: false },
{ id: 'hype', name: 'Hype sticker (if present)', critical: false },
{ id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true },
{ id: 'corners', name: 'Sleeve corners/edges detail', critical: false },
{ id: 'barcode', name: 'Barcode area', critical: false }
];
// Check if we have any photos at all
const hasPhotos = uploadedPhotos.length > 0;
const container = document.getElementById('shotList');
container.innerHTML = shotDefinitions.map(shot => {
const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1);
const statusClass = have ? 'completed' : shot.critical ? 'missing' : '';
const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500';
const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300';
const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle';
return `
${shot.name}
${shot.critical && !have ? 'CRITICAL ' : ''}
`}).join('');
feather.replace();
}
function copyHTML() {
const html = document.getElementById('htmlOutput');
html.select();
document.execCommand('copy');
showToast('HTML copied to clipboard!', 'success');
}
function copyTags() {
const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', ');
navigator.clipboard.writeText(tags);
showToast('Tags copied to clipboard!', 'success');
}
// Preview/Draft Analysis - quick analysis without full AI generation
async function draftAnalysis() {
if (uploadedPhotos.length === 0) {
showToast('Upload photos first for preview', 'error');
return;
}
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
// Show loading state
const dropZone = document.getElementById('dropZone');
const spinner = document.getElementById('uploadSpinner');
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
startAnalysisProgressSimulation();
try {
// Try OCR/AI analysis if available
const service = getAIService();
let ocrResult = null;
if (service && service.apiKey && uploadedPhotos.length > 0) {
try {
ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed
populateFieldsFromOCR(ocrResult);
} catch (e) {
console.log('Preview OCR failed:', e);
}
}
// Generate quick preview results
const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || '';
const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || '';
const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist';
const detectedTitle = title || ocrResult?.title || 'Unknown Title';
const baseTitle = `${detectedArtist} - ${detectedTitle}`;
// Generate quick titles
const quickTitles = [
`${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80),
`${baseTitle} Original Pressing Vinyl LP`.substring(0, 80),
`${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80)
].map((t, i) => ({
text: t,
chars: t.length,
style: ['Quick', 'Standard', 'Compact'][i]
}));
// Quick pricing estimate based on condition
const cost = parseFloat(document.getElementById('costInput').value) || 10;
const vinylCond = document.getElementById('vinylConditionInput').value;
const sleeveCond = document.getElementById('sleeveConditionInput').value;
const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 };
const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3;
const estimatedValue = Math.round(cost * Math.max(condMult, 1.5));
const suggestedPrice = Math.round(estimatedValue * 0.9);
// Render preview results
renderTitleOptions(quickTitles);
// Quick pricing card
document.getElementById('pricingStrategy').innerHTML = `
QUICK ESTIMATE
£${suggestedPrice}
Suggested Buy It Now
Est. Value: £${estimatedValue}
Your Cost: £${cost.toFixed(2)}
Condition: ${vinylCond}/${sleeveCond}
Preview Notes
${ocrResult ?
`
✓ AI detected information from photos
` :
`
⚠ Add API key in Settings for auto-detection
`
}
This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.
${ocrResult ? `
Detected from photos:
${ocrResult.artist ? `• Artist: ${ocrResult.artist} ` : ''}
${ocrResult.title ? `• Title: ${ocrResult.title} ` : ''}
${ocrResult.catalogueNumber ? `• Cat#: ${ocrResult.catalogueNumber} ` : ''}
${ocrResult.year ? `• Year: ${ocrResult.year} ` : ''}
` : ''}
`;
// Simple fee floor
const fees = suggestedPrice * 0.16;
const safeFloor = Math.ceil(cost + fees + 6);
document.getElementById('feeFloor').innerHTML = `
Your Cost
£${cost.toFixed(2)}
Est. Fees
£${fees.toFixed(2)}
`;
// Preview HTML description
const previewHtml = `
${detectedArtist} - ${detectedTitle}
${year ? `
Year: ${year}
` : ''}
${catNo ? `
Catalogue #: ${catNo}
` : ''}
Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}
[Full description will be generated with complete market analysis]
`;
const htmlOutput = document.getElementById('htmlOutput');
if (htmlOutput) htmlOutput.value = previewHtml;
// Preview tags
const previewTags = [
detectedArtist,
detectedTitle,
'vinyl',
'record',
vinylCond,
'lp',
year || 'vintage'
].filter(Boolean);
const tagsOutput = document.getElementById('tagsOutput');
if (tagsOutput) {
tagsOutput.innerHTML = previewTags.map(t => `
${t}
`).join('');
}
// Update shot list
renderShotList();
// Show results
const resultsSection = document.getElementById('resultsSection');
const emptyState = document.getElementById('emptyState');
if (resultsSection) resultsSection.classList.remove('hidden');
if (emptyState) emptyState.classList.add('hidden');
if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' });
showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success');
} catch (error) {
console.error('Preview error:', error);
showToast('Preview failed: ' + error.message, 'error');
} finally {
stopAnalysisProgress();
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
async function callAI(messages, temperature = 0.7) {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}`
},
body: JSON.stringify({
model: localStorage.getItem('deepseek_model') || 'deepseek-chat',
messages: messages,
temperature: temperature,
max_tokens: 2000
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'DeepSeek API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`DeepSeek Error: ${error.message}`, 'error');
return null;
}
} else {
// Fallback to OpenAI
const apiKey = localStorage.getItem('openai_api_key');
const model = localStorage.getItem('openai_model') || 'gpt-4o';
const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
if (!apiKey) {
showToast('OpenAI API key not configured. Go to Settings.', 'error');
return null;
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: messages,
temperature: temperature,
max_tokens: maxTokens
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`OpenAI Error: ${error.message}`, 'error');
return null;
}
}
}
// Legacy alias for backward compatibility
async function callOpenAI(messages, temperature = 0.7) {
return callAI(messages, temperature);
}
// Delete hosted image from imgBB
async function deleteHostedImage(deleteUrl) {
if (!deleteUrl) return false;
try {
const response = await fetch(deleteUrl, { method: 'GET' });
// imgBB delete URLs work via GET request
return response.ok;
} catch (error) {
console.error('Failed to delete image:', error);
return false;
}
}
// Get hosted photo URLs for eBay HTML description
function getHostedPhotoUrlsForEbay() {
return hostedPhotoUrls.map(img => ({
full: img.url,
display: img.displayUrl || img.url,
thumb: img.thumb,
medium: img.medium,
viewer: img.viewerUrl
}));
}
async function generateListingWithAI() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
if (!artist || !title) {
showToast('Please enter at least artist and title', 'error');
return;
}
const messages = [
{
role: 'system',
content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
},
{
role: 'user',
content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
}
];
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success');
const result = await callAI(messages, 0.7);
if (result) {
try {
const data = JSON.parse(result);
// Populate the UI with AI-generated content
if (data.titles) {
renderTitleOptions(data.titles.map(t => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: 'AI Generated'
})));
}
if (data.description) {
document.getElementById('htmlOutput').value = data.description;
}
if (data.tags) {
const tagsContainer = document.getElementById('tagsOutput');
tagsContainer.innerHTML = data.tags.map(t => `
${t}
`).join('');
}
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
showToast('AI listing generated!', 'success');
} catch (e) {
// If not valid JSON, treat as plain text description
document.getElementById('htmlOutput').value = result;
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
}
}
}
function requestHelp() {
alert(`VINYL PHOTO GUIDE:
ESSENTIAL SHOTS (need these):
• Front cover - square, no glare, color accurate
• Back cover - full frame, readable text
• Both labels - close enough to read all text
• Deadwax/runout - for pressing identification
CONDITION SHOTS:
• Vinyl in raking light at angle (shows scratches)
• Sleeve edges and corners
• Any flaws clearly documented
OPTIONARY BUT HELPFUL:
• Inner sleeve condition
• Inserts, posters, extras
• Hype stickers
• Barcode area
TIPS:
- Use natural daylight or 5500K bulbs
- Avoid flash directly on glossy sleeves
- Include scale reference if unusual size
- Photograph flaws honestly - reduces returns`);
}
function showToast(message, type = 'success') {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const iconMap = {
success: 'check',
error: 'alert-circle',
warning: 'alert-triangle'
};
const colorMap = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400'
};
const toast = document.createElement('div');
toast.className = `toast ${type} flex items-center gap-3`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
feather.replace();
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Cleanup function to delete all hosted images for current listing
async function cleanupHostedImages() {
if (window.currentListingImages) {
for (const img of window.currentListingImages) {
if (img.deleteUrl) {
await deleteHostedImage(img.deleteUrl);
}
}
window.currentListingImages = [];
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('VinylVault Pro initialized');
// Initialize drop zone
initDropZone();
// Warn about unsaved changes when leaving page with hosted images
window.addEventListener('beforeunload', (e) => {
if (hostedPhotoUrls.length > 0 && !window.listingPublished) {
// Optional: could add cleanup here or warn user
}
});
});
: '€';
// Mock comp research results
const comps = {
nm: { low: 45, high: 65, median: 52 },
vgplus: { low: 28, high: 42, median: 34 },
vg: { low: 15, high: 25, median: 19 }
};
// Calculate recommended price based on goal
let recommendedBin, strategy;
switch(goal) {
case 'quick':
recommendedBin = Math.round(comps.vgplus.low * 0.9);
strategy = 'BIN + Best Offer (aggressive)';
break;
case 'max':
recommendedBin = Math.round(comps.nm.high * 1.1);
strategy = 'BIN only, no offers, long duration';
break;
default:
recommendedBin = comps.vgplus.median;
strategy = 'BIN + Best Offer (standard)';
}
// Fee calculation (eBay UK approx)
const ebayFeeRate = 0.13; // 13% final value fee
const paypalRate = 0.029; // 2.9% + 30p
const fixedFee = 0.30;
const shippingCost = 4.50; // Estimated
const packingCost = 1.50;
const totalFees = (recommendedBin * ebayFeeRate) + (recommendedBin * paypalRate) + fixedFee;
const breakEven = cost + totalFees + shippingCost + packingCost;
const safeFloor = Math.ceil(breakEven * 1.05); // 5% buffer
// Generate titles
const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`;
const titles = generateTitles(baseTitle, catNo, year, goal);
// Render results
renderTitleOptions(titles);
renderPricingStrategy(recommendedBin, strategy, comps, currency, goal);
renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency);
await renderHTMLDescription(data, titles[0]);
renderTags(artist, title, catNo, year);
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
currentAnalysis = {
titles, recommendedBin, strategy, breakEven, safeFloor, currency
};
}
function generateTitles(base, catNo, year, goal) {
const titles = [];
const cat = catNo || 'CAT#';
const yr = year || 'YEAR';
const country = window.detectedCountry || 'UK';
const genre = window.detectedGenre || 'Rock';
const format = window.detectedFormat?.includes('7"') ? '7"' : window.detectedFormat?.includes('12"') ? '12"' : 'LP';
// Option 1: Classic collector focus
titles.push(`${base} ${format} ${yr} ${country} 1st Press ${cat} EX/VG+`);
// Option 2: Condition forward
titles.push(`NM! ${base} Original ${yr} Vinyl ${format} ${cat} Nice Copy`);
// Option 3: Rarity/hype with detected genre
titles.push(`${base} ${yr} ${country} Press ${cat} Rare Vintage ${genre} ${format}`);
// Option 4: Clean searchable
titles.push(`${base} Vinyl ${format} ${yr} ${cat} Excellent Condition`);
// Option 5: Genre tagged
titles.push(`${base} ${yr} ${format} ${genre} ${cat} VG+ Plays Great`);
return titles.map((t, i) => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: ['Classic Collector', 'Condition Forward', 'Rarity Focus', 'Clean Search', 'Genre Tagged'][i]
}));
}
function renderTitleOptions(titles) {
const container = document.getElementById('titleOptions');
container.innerHTML = titles.map((t, i) => `
${t.chars}/80
${t.text}
${t.style}
`).join('');
}
function selectTitle(el, text) {
document.querySelectorAll('.title-option').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
// Update clipboard copy
navigator.clipboard.writeText(text);
showToast('Title copied to clipboard!', 'success');
}
function renderPricingStrategy(bin, strategy, comps, currency, goal) {
const container = document.getElementById('pricingStrategy');
const offerSettings = goal === 'max' ? 'Offers: OFF' :
`Auto-accept: ${currency}${Math.floor(bin * 0.85)} | Auto-decline: ${currency}${Math.floor(bin * 0.7)}`;
container.innerHTML = `
RECOMMENDED
${currency}${bin}
Buy It Now
Strategy: ${strategy}
Best Offer: ${offerSettings}
Duration: 30 days (GTC)
Sold Comps by Grade
NM/NM-
${currency}${comps.nm.low}-${comps.nm.high} (med: ${comps.nm.median})
VG+/EX
${currency}${comps.vgplus.low}-${comps.vgplus.high} (med: ${comps.vgplus.median})
VG/VG+
${currency}${comps.vg.low}-${comps.vg.high} (med: ${comps.vg.median})
Based on last 90 days sold listings, same pressing. Prices exclude postage.
`;
}
function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
const container = document.getElementById('feeFloor');
container.innerHTML = `
Your Cost
${currency}${cost.toFixed(2)}
Est. Fees
${currency}${fees.toFixed(2)}
~16% total
Ship + Pack
${currency}${(shipping + packing).toFixed(2)}
Safe Floor Price
${currency}${safeFloor}
Auto-decline below this
`;
}
async function renderHTMLDescription(data, titleObj) {
const { artist, title, catNo, year } = data;
// Use hosted URL if available, otherwise fallback to local object URL
let heroImg = '';
let galleryImages = [];
if (hostedPhotoUrls.length > 0) {
heroImg = hostedPhotoUrls[0].displayUrl || hostedPhotoUrls[0].url;
galleryImages = hostedPhotoUrls.slice(1).map(img => img.displayUrl || img.url);
} else if (uploadedPhotos.length > 0) {
heroImg = URL.createObjectURL(uploadedPhotos[0]);
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
}
// Use OCR-detected values if available
const detectedLabel = window.detectedLabel || '[Verify from photos]';
const detectedCountry = window.detectedCountry || 'UK';
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
const detectedGenre = window.detectedGenre || 'rock';
const detectedCondition = window.detectedCondition || 'VG+/VG+';
const detectedPressingInfo = window.detectedPressingInfo || '';
// Fetch tracklist and detailed info from Discogs if available
let tracklistHtml = '';
let pressingDetailsHtml = '';
let provenanceHtml = '';
if (window.discogsReleaseId && window.discogsService?.key) {
try {
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
if (discogsData && discogsData.tracklist) {
// Build tracklist HTML
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
if (hasSideBreakdown) {
// Group by sides
const sides = {};
discogsData.tracklist.forEach(track => {
const side = track.position ? track.position.charAt(0) : 'Other';
if (!sides[side]) sides[side] = [];
sides[side].push(track);
});
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
Side ${side}
${tracks.map(track => `
${track.position} ${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`).join('');
} else {
// Simple list
tracklistHtml = `
${discogsData.tracklist.map(track => `
${track.position ? `${track.position} ` : ''}${track.title}
${track.duration ? `${track.duration} ` : ''}
`).join('')}
`;
}
// Build pressing/variation details
const identifiers = discogsData.identifiers || [];
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
pressingDetailsHtml = `
Pressing & Matrix Information
${barcodeInfo ? `
Barcode: ${barcodeInfo.value}
` : ''}
${matrixInfo.map(m => `
${m.type}: ${m.value}${m.description ? ` (${m.description}) ` : ''}
`).join('')}
${pressingInfo.map(p => `
${p.type}: ${p.value}
`).join('')}
${discogsData.notes ? `
${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}
` : ''}
`;
}
// Build provenance data for buyer confidence
const companies = discogsData.companies || [];
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
if (masteredBy || pressedBy || lacquerCut) {
provenanceHtml = `
Provenance & Production
${masteredBy ? `
✓ Mastered at ${masteredBy.name}
` : ''}
${lacquerCut ? `
✓ Lacquer cut at ${lacquerCut.name}
` : ''}
${pressedBy ? `
✓ Pressed at ${pressedBy.name}
` : ''}
${discogsData.num_for_sale ? `
Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs
` : ''}
`;
}
}
} catch (e) {
console.error('Failed to fetch Discogs details for HTML:', e);
}
}
// If no tracklist from Discogs, provide placeholder
if (!tracklistHtml) {
tracklistHtml = `Tracklist verification recommended. Please compare with Discogs entry for accuracy.
`;
}
const galleryHtml = galleryImages.length > 0 ? `
${galleryImages.map(url => `
`).join('')}
` : '';
const html = `
${galleryHtml}
Original ${detectedCountry} Pressing
${year || '1970s'}
${detectedFormat}
${detectedCondition}
Artist
${artist || 'See title'}
Title
${title || 'See title'}
Label
${detectedLabel}
Catalogue
${catNo || '[See photos]'}
Country
${detectedCountry}
Year
${year || '[Verify]'}
Condition Report
Vinyl: VG+ — Light surface marks, plays cleanly with minimal surface noise. No skips or jumps. [Adjust based on actual inspection]
Sleeve: VG+ — Minor edge wear, light ring wear visible under raking light. No splits or writing. [Adjust based on actual inspection]
Inner Sleeve: Original paper inner included, small split at bottom seam. [Verify/Adjust]
About This Release
${detectedGenre ? `${detectedGenre.charAt(0).toUpperCase() + detectedGenre.slice(1)} release` : 'Vintage vinyl release'}${detectedPressingInfo ? `. Matrix/Runout: ${detectedPressingInfo}` : ''}. [Add accurate description based on verified pressing details. Mention notable features: gatefold, insert, poster, hype sticker, etc.]
Tracklist
${tracklistHtml}
${pressingDetailsHtml}
${provenanceHtml}
Packing & Postage
Records are removed from outer sleeves to prevent seam splits during transit. Packed with stiffeners in a dedicated LP mailer. Royal Mail 48 Tracked or courier service.
Combined postage: Discount available for multiple purchases—please request invoice before payment.
Questions? Need more photos?
Message me anytime—happy to provide additional angles, audio clips, or pressing details.
`;
// Store reference to hosted images for potential cleanup
window.currentListingImages = hostedPhotoUrls.map(img => ({
url: img.url,
deleteUrl: img.deleteUrl
}));
document.getElementById('htmlOutput').value = html;
}
function renderTags(artist, title, catNo, year) {
const genre = window.detectedGenre || 'rock';
const format = window.detectedFormat?.toLowerCase().includes('7"') ? '7 inch' :
window.detectedFormat?.toLowerCase().includes('12"') ? '12 inch single' : 'lp';
const country = window.detectedCountry?.toLowerCase() || 'uk';
const tags = [
artist || 'vinyl',
title || 'record',
format,
'vinyl record',
'original pressing',
`${country} pressing`,
year || 'vintage',
catNo || '',
genre,
genre === 'rock' ? 'prog rock' : genre,
genre === 'rock' ? 'psych' : '',
'collector',
'audiophile',
format === 'lp' ? '12 inch' : format,
'33 rpm',
format === 'lp' ? 'album' : 'single',
'used vinyl',
'graded',
'excellent condition',
'rare vinyl',
'classic rock',
'vintage vinyl',
'record collection',
'music',
'audio',
window.detectedLabel || ''
].filter(Boolean);
const container = document.getElementById('tagsOutput');
container.innerHTML = tags.map(t => `
${t}
`).join('');
}
function renderShotList() {
// Map shot types to display info
const shotDefinitions = [
{ id: 'front', name: 'Front cover (square, well-lit)', critical: true },
{ id: 'back', name: 'Back cover (full shot)', critical: true },
{ id: 'spine', name: 'Spine (readable text)', critical: true },
{ id: 'label_a', name: 'Label Side A (close, legible)', critical: true },
{ id: 'label_b', name: 'Label Side B (close, legible)', critical: true },
{ id: 'deadwax', name: 'Deadwax/runout grooves', critical: true },
{ id: 'inner', name: 'Inner sleeve (both sides)', critical: false },
{ id: 'insert', name: 'Insert/poster if included', critical: false },
{ id: 'hype', name: 'Hype sticker (if present)', critical: false },
{ id: 'vinyl', name: 'Vinyl in raking light (flaws)', critical: true },
{ id: 'corners', name: 'Sleeve corners/edges detail', critical: false },
{ id: 'barcode', name: 'Barcode area', critical: false }
];
// Check if we have any photos at all
const hasPhotos = uploadedPhotos.length > 0;
const container = document.getElementById('shotList');
container.innerHTML = shotDefinitions.map(shot => {
const have = detectedPhotoTypes.has(shot.id) || (shot.id === 'front' && hasPhotos) || (shot.id === 'back' && uploadedPhotos.length > 1);
const statusClass = have ? 'completed' : shot.critical ? 'missing' : '';
const iconColor = have ? 'text-green-500' : shot.critical ? 'text-yellow-500' : 'text-gray-500';
const textClass = have ? 'text-gray-400 line-through' : 'text-gray-300';
const icon = have ? 'check-circle' : shot.critical ? 'alert-circle' : 'circle';
return `
${shot.name}
${shot.critical && !have ? 'CRITICAL ' : ''}
`}).join('');
feather.replace();
}
function copyHTML() {
const html = document.getElementById('htmlOutput');
html.select();
document.execCommand('copy');
showToast('HTML copied to clipboard!', 'success');
}
function copyTags() {
const tags = Array.from(document.querySelectorAll('#tagsOutput span')).map(s => s.textContent).join(', ');
navigator.clipboard.writeText(tags);
showToast('Tags copied to clipboard!', 'success');
}
async function draftAnalysis() {
if (uploadedPhotos.length === 0) {
showToast('Upload photos first for preview', 'error');
return;
}
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
// Show loading state
const dropZone = document.getElementById('dropZone');
const spinner = document.getElementById('uploadSpinner');
spinner.classList.remove('hidden');
dropZone.classList.add('pointer-events-none');
startAnalysisProgressSimulation();
try {
// Try OCR/AI analysis if available
const service = getAIService();
let ocrResult = null;
if (service && service.apiKey && uploadedPhotos.length > 0) {
try {
ocrResult = await service.analyzeRecordImages(uploadedPhotos.slice(0, 2)); // Limit to 2 photos for speed
populateFieldsFromOCR(ocrResult);
} catch (e) {
console.log('Preview OCR failed:', e);
}
}
// Generate quick preview results
const catNo = document.getElementById('catInput').value.trim() || ocrResult?.catalogueNumber || '';
const year = document.getElementById('yearInput').value.trim() || ocrResult?.year || '';
const detectedArtist = artist || ocrResult?.artist || 'Unknown Artist';
const detectedTitle = title || ocrResult?.title || 'Unknown Title';
const baseTitle = `${detectedArtist} - ${detectedTitle}`;
// Generate quick titles
const quickTitles = [
`${baseTitle} ${year ? `(${year})` : ''} ${catNo} VG+`.substring(0, 80),
`${baseTitle} Original Pressing Vinyl LP`.substring(0, 80),
`${detectedArtist} ${detectedTitle} ${catNo || 'LP'}`.substring(0, 80)
].map((t, i) => ({
text: t,
chars: t.length,
style: ['Quick', 'Standard', 'Compact'][i]
}));
// Quick pricing estimate based on condition
const cost = parseFloat(document.getElementById('costInput').value) || 10;
const vinylCond = document.getElementById('vinylConditionInput').value;
const sleeveCond = document.getElementById('sleeveConditionInput').value;
const conditionMultipliers = { 'M': 3, 'NM': 2.5, 'VG+': 1.8, 'VG': 1.2, 'G+': 0.8, 'G': 0.5 };
const condMult = (conditionMultipliers[vinylCond] || 1) * 0.7 + (conditionMultipliers[sleeveCond] || 1) * 0.3;
const estimatedValue = Math.round(cost * Math.max(condMult, 1.5));
const suggestedPrice = Math.round(estimatedValue * 0.9);
// Render preview results
renderTitleOptions(quickTitles);
// Quick pricing card
document.getElementById('pricingStrategy').innerHTML = `
QUICK ESTIMATE
£${suggestedPrice}
Suggested Buy It Now
Est. Value: £${estimatedValue}
Your Cost: £${cost.toFixed(2)}
Condition: ${vinylCond}/${sleeveCond}
Preview Notes
${ocrResult ?
`
✓ AI detected information from photos
` :
`
⚠ Add API key in Settings for auto-detection
`
}
This is a quick estimate based on your cost and condition. Run "Generate Full Listing" for complete market analysis, sold comps, and optimized pricing.
${ocrResult ? `
Detected from photos:
${ocrResult.artist ? `• Artist: ${ocrResult.artist} ` : ''}
${ocrResult.title ? `• Title: ${ocrResult.title} ` : ''}
${ocrResult.catalogueNumber ? `• Cat#: ${ocrResult.catalogueNumber} ` : ''}
${ocrResult.year ? `• Year: ${ocrResult.year} ` : ''}
` : ''}
`;
// Simple fee floor
const fees = suggestedPrice * 0.16;
const safeFloor = Math.ceil(cost + fees + 6);
document.getElementById('feeFloor').innerHTML = `
Your Cost
£${cost.toFixed(2)}
Est. Fees
£${fees.toFixed(2)}
`;
// Preview HTML description
const previewHtml = `
${detectedArtist} - ${detectedTitle}
${year ? `
Year: ${year}
` : ''}
${catNo ? `
Catalogue #: ${catNo}
` : ''}
Condition: Vinyl ${vinylCond}, Sleeve ${sleeveCond}
[Full description will be generated with complete market analysis]
`;
document.getElementById('htmlOutput').value = previewHtml;
// Preview tags
const previewTags = [
detectedArtist,
detectedTitle,
'vinyl',
'record',
vinylCond,
'lp',
year || 'vintage'
].filter(Boolean);
document.getElementById('tagsOutput').innerHTML = previewTags.map(t => `
${t}
`).join('');
// Update shot list
renderShotList();
// Show results
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
resultsSection.scrollIntoView({ behavior: 'smooth' });
showToast('Quick preview ready! Click "Generate Full Listing" for complete analysis.', 'success');
} catch (error) {
console.error('Preview error:', error);
showToast('Preview failed: ' + error.message, 'error');
} finally {
stopAnalysisProgress();
setTimeout(() => {
spinner.classList.add('hidden');
dropZone.classList.remove('pointer-events-none');
updateAnalysisProgress('Initializing...', 0);
}, 300);
}
}
async function callAI(messages, temperature = 0.7) {
const provider = localStorage.getItem('ai_provider') || 'openai';
if (provider === 'deepseek' && window.deepseekService?.isConfigured) {
try {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('deepseek_api_key')}`
},
body: JSON.stringify({
model: localStorage.getItem('deepseek_model') || 'deepseek-chat',
messages: messages,
temperature: temperature,
max_tokens: 2000
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'DeepSeek API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`DeepSeek Error: ${error.message}`, 'error');
return null;
}
} else {
// Fallback to OpenAI
const apiKey = localStorage.getItem('openai_api_key');
const model = localStorage.getItem('openai_model') || 'gpt-4o';
const maxTokens = parseInt(localStorage.getItem('openai_max_tokens')) || 2000;
if (!apiKey) {
showToast('OpenAI API key not configured. Go to Settings.', 'error');
return null;
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: model,
messages: messages,
temperature: temperature,
max_tokens: maxTokens
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'API request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
showToast(`OpenAI Error: ${error.message}`, 'error');
return null;
}
}
}
// Legacy alias for backward compatibility
async function callOpenAI(messages, temperature = 0.7) {
return callAI(messages, temperature);
}
// Delete hosted image from imgBB
async function deleteHostedImage(deleteUrl) {
if (!deleteUrl) return false;
try {
const response = await fetch(deleteUrl, { method: 'GET' });
// imgBB delete URLs work via GET request
return response.ok;
} catch (error) {
console.error('Failed to delete image:', error);
return false;
}
}
// Get hosted photo URLs for eBay HTML description
function getHostedPhotoUrlsForEbay() {
return hostedPhotoUrls.map(img => ({
full: img.url,
display: img.displayUrl || img.url,
thumb: img.thumb,
medium: img.medium,
viewer: img.viewerUrl
}));
}
async function generateListingWithAI() {
const artist = document.getElementById('artistInput').value.trim();
const title = document.getElementById('titleInput').value.trim();
const catNo = document.getElementById('catInput').value.trim();
const year = document.getElementById('yearInput').value.trim();
if (!artist || !title) {
showToast('Please enter at least artist and title', 'error');
return;
}
const messages = [
{
role: 'system',
content: 'You are a vinyl record eBay listing expert. Generate optimized titles, descriptions, and pricing strategies. Always return JSON format with: titles (array), description (string), condition_notes (string), price_estimate (object with min, max, recommended), and tags (array).'
},
{
role: 'user',
content: `Generate an eBay listing for: ${artist} - ${title}${catNo ? ` (Catalog: ${catNo})` : ''}${year ? ` (${year})` : ''}. Include optimized title options, professional HTML description, condition guidance, price estimate in GBP, and relevant tags.`
}
];
const provider = localStorage.getItem('ai_provider') || 'openai';
showToast(`Generating listing with ${provider === 'deepseek' ? 'DeepSeek' : 'OpenAI'}...`, 'success');
const result = await callAI(messages, 0.7);
if (result) {
try {
const data = JSON.parse(result);
// Populate the UI with AI-generated content
if (data.titles) {
renderTitleOptions(data.titles.map(t => ({
text: t.length > 80 ? t.substring(0, 77) + '...' : t,
chars: Math.min(t.length, 80),
style: 'AI Generated'
})));
}
if (data.description) {
document.getElementById('htmlOutput').value = data.description;
}
if (data.tags) {
const tagsContainer = document.getElementById('tagsOutput');
tagsContainer.innerHTML = data.tags.map(t => `
${t}
`).join('');
}
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
showToast('AI listing generated!', 'success');
} catch (e) {
// If not valid JSON, treat as plain text description
document.getElementById('htmlOutput').value = result;
resultsSection.classList.remove('hidden');
emptyState.classList.add('hidden');
}
}
}
function requestHelp() {
alert(`VINYL PHOTO GUIDE:
ESSENTIAL SHOTS (need these):
• Front cover - square, no glare, color accurate
• Back cover - full frame, readable text
• Both labels - close enough to read all text
• Deadwax/runout - for pressing identification
CONDITION SHOTS:
• Vinyl in raking light at angle (shows scratches)
• Sleeve edges and corners
• Any flaws clearly documented
OPTIONARY BUT HELPFUL:
• Inner sleeve condition
• Inserts, posters, extras
• Hype stickers
• Barcode area
TIPS:
- Use natural daylight or 5500K bulbs
- Avoid flash directly on glossy sleeves
- Include scale reference if unusual size
- Photograph flaws honestly - reduces returns`);
}
function showToast(message, type = 'success') {
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const iconMap = {
success: 'check',
error: 'alert-circle',
warning: 'alert-triangle'
};
const colorMap = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400'
};
const toast = document.createElement('div');
toast.className = `toast ${type} flex items-center gap-3`;
toast.innerHTML = `
${message}
`;
document.body.appendChild(toast);
feather.replace();
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Cleanup function to delete all hosted images for current listing
async function cleanupHostedImages() {
if (window.currentListingImages) {
for (const img of window.currentListingImages) {
if (img.deleteUrl) {
await deleteHostedImage(img.deleteUrl);
}
}
window.currentListingImages = [];
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('VinylVault Pro initialized');
// Initialize drop zone
initDropZone();
// Warn about unsaved changes when leaving page with hosted images
window.addEventListener('beforeunload', (e) => {
if (hostedPhotoUrls.length > 0 && !window.listingPublished) {
// Optional: could add cleanup here or warn user
}
});
});