diff --git a/public/app.js b/public/app.js index f8b3c24..8888d72 100644 --- a/public/app.js +++ b/public/app.js @@ -25,6 +25,9 @@ const elements = { settingsModal: document.getElementById('settings-modal'), settingsApiForm: document.getElementById('settings-api-form'), settingsGeminiKeyInput: document.getElementById('settings-gemini-key'), + settingsGeminiModelSelect: document.getElementById('settings-gemini-model'), + btnDiscoverModels: document.getElementById('btn-discover-models'), + modelsStatusHint: document.getElementById('models-status-hint'), btnToggleGeminiKey: document.getElementById('btn-toggle-gemini-key'), settingsStatusMsg: document.getElementById('settings-status-msg'), settingsStatusText: document.getElementById('settings-status-text'), @@ -259,6 +262,9 @@ function registerEvents() { elements.btnSettings.addEventListener('click', openSettingsModal); elements.settingsApiForm.addEventListener('submit', handleSaveSettings); elements.btnToggleGeminiKey.addEventListener('click', toggleSettingsKeyVisibility); + if (elements.btnDiscoverModels) { + elements.btnDiscoverModels.addEventListener('click', () => handleDiscoverModels(true)); + } // Image Scanning Actions elements.btnScanImage.addEventListener('click', () => elements.scanImageInput.click()); @@ -2024,6 +2030,91 @@ window.toggleRecipeManagerRole = async function(id, makeManager) { // SYSTEM SETTINGS & GEMINI AI PHOTO SCANNER // ------------------------------------------------------------- +function populateModelDropdown(models, selectedModel) { + if (!elements.settingsGeminiModelSelect) return; + const select = elements.settingsGeminiModelSelect; + select.innerHTML = ''; + + const targetModel = selectedModel || 'gemini-2.0-flash'; + let foundTarget = false; + + models.forEach(m => { + const opt = document.createElement('option'); + opt.value = m.id; + opt.textContent = m.displayName || m.id; + if (m.id === 'gemini-2.0-flash') { + opt.textContent = `${m.displayName || 'Gemini 2.0 Flash'} (Recommended)`; + } + if (m.id === targetModel) { + opt.selected = true; + foundTarget = true; + } + select.appendChild(opt); + }); + + if (!foundTarget && targetModel) { + const customOpt = document.createElement('option'); + customOpt.value = targetModel; + customOpt.textContent = targetModel; + customOpt.selected = true; + select.appendChild(customOpt); + } +} + +async function handleDiscoverModels(showNotice = true) { + const key = elements.settingsGeminiKeyInput ? elements.settingsGeminiKeyInput.value.trim() : ''; + if (elements.modelsStatusHint) { + elements.modelsStatusHint.textContent = 'Discovering available models from Google API...'; + elements.modelsStatusHint.style.color = 'var(--primary)'; + } + if (elements.btnDiscoverModels) { + elements.btnDiscoverModels.disabled = true; + } + + try { + const res = await fetch('/api/settings/discover-models', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders() + }, + body: JSON.stringify({ apiKey: key }) + }); + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || 'Failed to discover models.'); + } + + if (data.models && data.models.length > 0) { + const currentSelected = elements.settingsGeminiModelSelect ? elements.settingsGeminiModelSelect.value : 'gemini-2.0-flash'; + populateModelDropdown(data.models, currentSelected); + if (elements.modelsStatusHint) { + elements.modelsStatusHint.textContent = `Discovered ${data.models.length} compatible models.`; + elements.modelsStatusHint.style.color = 'var(--text-muted)'; + } + } else { + if (elements.modelsStatusHint) { + elements.modelsStatusHint.textContent = 'No compatible models found for this key.'; + elements.modelsStatusHint.style.color = '#ef4444'; + } + } + } catch (err) { + console.error('Discover models error:', err); + if (elements.modelsStatusHint) { + elements.modelsStatusHint.textContent = `Discovery error: ${err.message}`; + elements.modelsStatusHint.style.color = '#ef4444'; + } + if (showNotice) { + alert(`Model Discovery Failed: ${err.message}\n\nPlease check that your Gemini API Key is valid.`); + } + } finally { + if (elements.btnDiscoverModels) { + elements.btnDiscoverModels.disabled = false; + } + } +} + async function openSettingsModal() { // Reset visibility states and statuses elements.settingsStatusMsg.style.display = 'none'; @@ -2034,6 +2125,10 @@ async function openSettingsModal() { elements.settingsGeminiKeyInput.value = ''; elements.settingsGeminiKeyInput.placeholder = 'Loading credentials...'; + if (elements.modelsStatusHint) { + elements.modelsStatusHint.textContent = 'Used for both recipe photo scanning and AI text polishing.'; + elements.modelsStatusHint.style.color = 'var(--text-muted)'; + } openModal('settings-modal'); @@ -2056,6 +2151,25 @@ async function openSettingsModal() { if (key && !isPlaceholder) { elements.settingsGeminiKeyInput.value = key; } + + if (elements.settingsGeminiModelSelect) { + const savedModel = settings.geminiModel || 'gemini-2.0-flash'; + let exists = false; + for (let i = 0; i < elements.settingsGeminiModelSelect.options.length; i++) { + if (elements.settingsGeminiModelSelect.options[i].value === savedModel) { + exists = true; + break; + } + } + if (!exists && savedModel) { + const opt = document.createElement('option'); + opt.value = savedModel; + opt.textContent = savedModel; + elements.settingsGeminiModelSelect.appendChild(opt); + } + elements.settingsGeminiModelSelect.value = savedModel; + } + if (elements.settingsDefaultConsolidate && typeof settings.defaultConsolidateLists === 'boolean') { elements.settingsDefaultConsolidate.checked = settings.defaultConsolidateLists; applyUserDefaultConsolidationPreference(settings.defaultConsolidateLists); @@ -2071,6 +2185,7 @@ async function handleSaveSettings(e) { elements.settingsStatusMsg.style.display = 'none'; const key = elements.settingsGeminiKeyInput.value.trim(); + const model = elements.settingsGeminiModelSelect ? elements.settingsGeminiModelSelect.value : 'gemini-2.0-flash'; const defaultConsolidate = elements.settingsDefaultConsolidate ? elements.settingsDefaultConsolidate.checked : true; try { @@ -2082,6 +2197,7 @@ async function handleSaveSettings(e) { }, body: JSON.stringify({ geminiApiKey: key, + geminiModel: model, defaultConsolidateLists: defaultConsolidate }) }); @@ -2243,6 +2359,7 @@ async function scanRecipeFile(file) { } const settings = await settingsResponse.json(); const apiKey = settings.geminiApiKey ? settings.geminiApiKey.trim() : ''; + const geminiModel = settings.geminiModel ? settings.geminiModel.trim() : 'gemini-2.0-flash'; const isPlaceholder = apiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE' || apiKey === ''; if (!apiKey || isPlaceholder) { @@ -2272,7 +2389,7 @@ async function scanRecipeFile(file) { // 3. Make direct call to Google Gemini Vision API const promptText = "You are a professional culinary transcriber. Analyze the image of a recipe (which could be a cookbook page, handwritten note, screenshot, or packaging) and extract it accurately. Fill in all fields as completely as possible. The response must be a JSON object matching this schema:\n{\n \"title\": \"Recipe Title\",\n \"description\": \"Brief description or review\",\n \"servings\": \"e.g., 4\",\n \"prepTime\": \"e.g., 15 mins\",\n \"cookTime\": \"e.g., 30 mins\",\n \"totalTime\": \"e.g., 45 mins\",\n \"ingredients\": [\"ingredient 1\", \"ingredient 2\"],\n \"instructions\": [\"step 1\", \"step 2\"]\n}\nDo not include any markdown styling (like ```json) or explanation. Return only the raw JSON string."; - const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`; + const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${apiKey}`; const geminiResponse = await fetch(geminiUrl, { method: 'POST', @@ -2302,15 +2419,16 @@ async function scanRecipeFile(file) { if (!geminiResponse.ok) { const errorObj = await geminiResponse.json().catch(() => ({})); const status = geminiResponse.status; - let errMsg = 'Google Gemini API call failed.'; - if (status === 400) { - errMsg = 'Invalid parameters or model version. Please verify your settings.'; - } else if (status === 403) { + let errMsg = (errorObj.error && errorObj.error.message) + ? errorObj.error.message + : 'Google Gemini API call failed.'; + + if (status === 400 && (!errorObj.error || !errorObj.error.message)) { + errMsg = `Invalid parameters or model version (${geminiModel}). Please check your settings.`; + } else if (status === 403 && (!errorObj.error || !errorObj.error.message)) { errMsg = 'Access denied. The Google Gemini API key is invalid or inactive.'; - } else if (status === 429) { + } else if (status === 429 && (!errorObj.error || !errorObj.error.message)) { errMsg = 'Rate limit exceeded. You have made too many requests or hit your Gemini quota.'; - } else if (errorObj.error && errorObj.error.message) { - errMsg = `Gemini API Error: ${errorObj.error.message}`; } throw new Error(errMsg); } diff --git a/public/index.html b/public/index.html index 12da8e5..e6f4391 100644 --- a/public/index.html +++ b/public/index.html @@ -1013,11 +1013,11 @@
- Configure your system-wide AI credentials to enable physical recipe photo scanning. + Configure your system-wide Google Gemini credentials and model to enable recipe photo scanning and AI text polishing.