feat(ai): guard all Gemini features when API key is not configured
Added _geminiAvailable global flag (false by default):
- Set in _initApp() from serverSettings.gemini_key_set after app loads
- Updated in syncSettingsFromDB() so it stays current if key is added later
Added _requireGemini() helper:
- Returns true if Gemini key is configured → proceed normally
- Returns false + shows a warning toast if key is missing → abort
Added _updateGeminiButtonState():
- Adds .header-btn-no-ai CSS class to Gemini button when key is missing:
greyed out, slight grayscale filter, amber dot badge in corner
- Updates button tooltip to explain what to do
- Removes class/restores normal appearance when key is present
All 6 AI entry points now call _requireGemini() as first line:
captureForAI() — AI product identification from scan page
captureForAIFormFill() — AI product fill in manual add form
scanExpiryWithAI() — AI expiry date reader
openRecipeDialog() — recipe generation dialog
generateRecipe() — recipe generation (direct call path)
quickRecipeSuggestion() — quick expiring-products recipe (→ chat)
showPage('chat') — Gemini chat page
Previously: user would click the button, camera would open, API call
would fail, and only THEN see an error message deep in the flow.
Now: blocked immediately at the entry point with a clear toast.
This commit is contained in:
@@ -274,6 +274,26 @@ body {
|
||||
background: rgba(99, 102, 241, 0.55);
|
||||
}
|
||||
|
||||
/* When Gemini API key is not configured */
|
||||
.header-gemini-btn.header-btn-no-ai {
|
||||
opacity: 0.45;
|
||||
filter: grayscale(0.7);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
.header-gemini-btn.header-btn-no-ai::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #f59e0b;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.gemini-icon {
|
||||
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
|
||||
}
|
||||
|
||||
+40
-1
@@ -80,6 +80,35 @@ function reportError(payload) {
|
||||
// Fires on tab focus and every 5 minutes.
|
||||
const _loadedVersion = (document.querySelector('.header-version')?.textContent?.trim() || '').replace(/^v/, '');
|
||||
|
||||
// ── Gemini AI availability ────────────────────────────────────────────────────
|
||||
// Set to true in _initApp / syncSettingsFromDB once server confirms key is set.
|
||||
// All AI entry points call _requireGemini() before opening camera / API calls.
|
||||
let _geminiAvailable = false;
|
||||
|
||||
function _requireGemini() {
|
||||
if (_geminiAvailable) return true;
|
||||
showToast(
|
||||
'🤖 ' + t('error.no_api_key'),
|
||||
'warning',
|
||||
6000
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update Gemini button visual state to signal no key configured
|
||||
function _updateGeminiButtonState() {
|
||||
const btn = document.querySelector('.header-gemini-btn');
|
||||
if (!btn) return;
|
||||
if (_geminiAvailable) {
|
||||
btn.classList.remove('header-btn-no-ai');
|
||||
btn.removeAttribute('title');
|
||||
btn.setAttribute('title', 'Chat con Gemini');
|
||||
} else {
|
||||
btn.classList.add('header-btn-no-ai');
|
||||
btn.setAttribute('title', '🤖 Gemini non configurato — imposta GEMINI_API_KEY nelle impostazioni');
|
||||
}
|
||||
}
|
||||
|
||||
function _checkWebappUpdate() {
|
||||
const STORAGE_KEY = '_evershelf_update_checked_at';
|
||||
const SEEN_KEY = '_evershelf_update_seen_ts';
|
||||
@@ -1779,6 +1808,8 @@ async function syncSettingsFromDB() {
|
||||
try {
|
||||
// Primary: load from server .env
|
||||
const serverSettings = await api('get_settings');
|
||||
_geminiAvailable = !!(serverSettings.gemini_key_set || serverSettings.gemini_key);
|
||||
_updateGeminiButtonState();
|
||||
const s = getSettings();
|
||||
const serverKeys = ['default_persons','pref_veloce','pref_pocafame','pref_scadenze',
|
||||
'pref_healthy','pref_opened','pref_zerowaste','dietary','appliances',
|
||||
@@ -2265,7 +2296,7 @@ function showPage(pageId, param = null) {
|
||||
case 'log': loadLog(); break;
|
||||
case 'ai': initAICamera(); break;
|
||||
case 'settings': loadSettingsUI(); break;
|
||||
case 'chat': initChat(); break;
|
||||
case 'chat': if (_requireGemini()) initChat(); break;
|
||||
}
|
||||
|
||||
// Auto-refresh banner notifications while on dashboard (every 5 min)
|
||||
@@ -2865,6 +2896,7 @@ function openedFraction(item) {
|
||||
}
|
||||
|
||||
function quickRecipeSuggestion() {
|
||||
if (!_requireGemini()) return;
|
||||
// Navigate to chat and auto-send a prompt about expiring products
|
||||
showPage('chat');
|
||||
setTimeout(() => {
|
||||
@@ -7090,6 +7122,7 @@ async function submitUse(e) {
|
||||
|
||||
// ===== AI IDENTIFICATION =====
|
||||
async function captureForAI() {
|
||||
if (!_requireGemini()) return;
|
||||
stopScanner();
|
||||
showPage('ai');
|
||||
}
|
||||
@@ -7399,6 +7432,7 @@ async function saveAIProductDirect() {
|
||||
let _pfAiStream = null;
|
||||
|
||||
async function captureForAIFormFill() {
|
||||
if (!_requireGemini()) return;
|
||||
document.getElementById('modal-content').innerHTML = `
|
||||
<div class="modal-header">
|
||||
<h3>📷 ${t('scan.ai_identify')}</h3>
|
||||
@@ -8781,6 +8815,7 @@ async function addSelectedSuggestions() {
|
||||
let expiryStream = null;
|
||||
|
||||
async function scanExpiryWithAI() {
|
||||
if (!_requireGemini()) return;
|
||||
// Create modal for camera capture
|
||||
document.getElementById('modal-content').innerHTML = `
|
||||
<div class="modal-header">
|
||||
@@ -9435,6 +9470,7 @@ let _recipeVariationCount = {}; // { 'pranzo': 0, 'cena': 1, ... }
|
||||
let _rejectedRecipeIngredients = []; // ingredient names from previously rejected recipes
|
||||
|
||||
function openRecipeDialog() {
|
||||
if (!_requireGemini()) return;
|
||||
const meal = getMealType();
|
||||
const settings = getSettings();
|
||||
document.getElementById('recipe-overlay').style.display = 'flex';
|
||||
@@ -10600,6 +10636,7 @@ function regenerateRecipe() {
|
||||
}
|
||||
|
||||
async function generateRecipe() {
|
||||
if (!_requireGemini()) return;
|
||||
const meal = getSelectedMealType();
|
||||
const persons = parseInt(document.getElementById('recipe-persons').value) || 1;
|
||||
const settings = getSettings();
|
||||
@@ -11719,6 +11756,8 @@ async function _initApp() {
|
||||
// are taken into account before deciding which wizard steps to show.
|
||||
let serverSettings = {};
|
||||
try { serverSettings = await api('get_settings'); } catch(e) {}
|
||||
_geminiAvailable = !!(serverSettings.gemini_key_set || serverSettings.gemini_key);
|
||||
_updateGeminiButtonState();
|
||||
const missing = _getMissingSetupSteps(serverSettings);
|
||||
if (missing.length > 0) {
|
||||
showSetupWizard(missing);
|
||||
|
||||
Reference in New Issue
Block a user