mirror of
https://gitlab.com/Zer0interrupt/recipe-manager.git
synced 2026-11-01 23:24:11 -05:00
Support dynamic Gemini AI model selection with model discovery and default to gemini-2.0-flash
This commit is contained in:
parent
a098ea3bca
commit
311c03136c
4 changed files with 217 additions and 19 deletions
134
public/app.js
134
public/app.js
|
|
@ -25,6 +25,9 @@ const elements = {
|
||||||
settingsModal: document.getElementById('settings-modal'),
|
settingsModal: document.getElementById('settings-modal'),
|
||||||
settingsApiForm: document.getElementById('settings-api-form'),
|
settingsApiForm: document.getElementById('settings-api-form'),
|
||||||
settingsGeminiKeyInput: document.getElementById('settings-gemini-key'),
|
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'),
|
btnToggleGeminiKey: document.getElementById('btn-toggle-gemini-key'),
|
||||||
settingsStatusMsg: document.getElementById('settings-status-msg'),
|
settingsStatusMsg: document.getElementById('settings-status-msg'),
|
||||||
settingsStatusText: document.getElementById('settings-status-text'),
|
settingsStatusText: document.getElementById('settings-status-text'),
|
||||||
|
|
@ -259,6 +262,9 @@ function registerEvents() {
|
||||||
elements.btnSettings.addEventListener('click', openSettingsModal);
|
elements.btnSettings.addEventListener('click', openSettingsModal);
|
||||||
elements.settingsApiForm.addEventListener('submit', handleSaveSettings);
|
elements.settingsApiForm.addEventListener('submit', handleSaveSettings);
|
||||||
elements.btnToggleGeminiKey.addEventListener('click', toggleSettingsKeyVisibility);
|
elements.btnToggleGeminiKey.addEventListener('click', toggleSettingsKeyVisibility);
|
||||||
|
if (elements.btnDiscoverModels) {
|
||||||
|
elements.btnDiscoverModels.addEventListener('click', () => handleDiscoverModels(true));
|
||||||
|
}
|
||||||
|
|
||||||
// Image Scanning Actions
|
// Image Scanning Actions
|
||||||
elements.btnScanImage.addEventListener('click', () => elements.scanImageInput.click());
|
elements.btnScanImage.addEventListener('click', () => elements.scanImageInput.click());
|
||||||
|
|
@ -2024,6 +2030,91 @@ window.toggleRecipeManagerRole = async function(id, makeManager) {
|
||||||
// SYSTEM SETTINGS & GEMINI AI PHOTO SCANNER
|
// 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() {
|
async function openSettingsModal() {
|
||||||
// Reset visibility states and statuses
|
// Reset visibility states and statuses
|
||||||
elements.settingsStatusMsg.style.display = 'none';
|
elements.settingsStatusMsg.style.display = 'none';
|
||||||
|
|
@ -2034,6 +2125,10 @@ async function openSettingsModal() {
|
||||||
|
|
||||||
elements.settingsGeminiKeyInput.value = '';
|
elements.settingsGeminiKeyInput.value = '';
|
||||||
elements.settingsGeminiKeyInput.placeholder = 'Loading credentials...';
|
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');
|
openModal('settings-modal');
|
||||||
|
|
||||||
|
|
@ -2056,6 +2151,25 @@ async function openSettingsModal() {
|
||||||
if (key && !isPlaceholder) {
|
if (key && !isPlaceholder) {
|
||||||
elements.settingsGeminiKeyInput.value = key;
|
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') {
|
if (elements.settingsDefaultConsolidate && typeof settings.defaultConsolidateLists === 'boolean') {
|
||||||
elements.settingsDefaultConsolidate.checked = settings.defaultConsolidateLists;
|
elements.settingsDefaultConsolidate.checked = settings.defaultConsolidateLists;
|
||||||
applyUserDefaultConsolidationPreference(settings.defaultConsolidateLists);
|
applyUserDefaultConsolidationPreference(settings.defaultConsolidateLists);
|
||||||
|
|
@ -2071,6 +2185,7 @@ async function handleSaveSettings(e) {
|
||||||
elements.settingsStatusMsg.style.display = 'none';
|
elements.settingsStatusMsg.style.display = 'none';
|
||||||
|
|
||||||
const key = elements.settingsGeminiKeyInput.value.trim();
|
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;
|
const defaultConsolidate = elements.settingsDefaultConsolidate ? elements.settingsDefaultConsolidate.checked : true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -2082,6 +2197,7 @@ async function handleSaveSettings(e) {
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
geminiApiKey: key,
|
geminiApiKey: key,
|
||||||
|
geminiModel: model,
|
||||||
defaultConsolidateLists: defaultConsolidate
|
defaultConsolidateLists: defaultConsolidate
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
@ -2243,6 +2359,7 @@ async function scanRecipeFile(file) {
|
||||||
}
|
}
|
||||||
const settings = await settingsResponse.json();
|
const settings = await settingsResponse.json();
|
||||||
const apiKey = settings.geminiApiKey ? settings.geminiApiKey.trim() : '';
|
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 === '';
|
const isPlaceholder = apiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE' || apiKey === '';
|
||||||
|
|
||||||
if (!apiKey || isPlaceholder) {
|
if (!apiKey || isPlaceholder) {
|
||||||
|
|
@ -2272,7 +2389,7 @@ async function scanRecipeFile(file) {
|
||||||
// 3. Make direct call to Google Gemini Vision API
|
// 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 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, {
|
const geminiResponse = await fetch(geminiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -2302,15 +2419,16 @@ async function scanRecipeFile(file) {
|
||||||
if (!geminiResponse.ok) {
|
if (!geminiResponse.ok) {
|
||||||
const errorObj = await geminiResponse.json().catch(() => ({}));
|
const errorObj = await geminiResponse.json().catch(() => ({}));
|
||||||
const status = geminiResponse.status;
|
const status = geminiResponse.status;
|
||||||
let errMsg = 'Google Gemini API call failed.';
|
let errMsg = (errorObj.error && errorObj.error.message)
|
||||||
if (status === 400) {
|
? errorObj.error.message
|
||||||
errMsg = 'Invalid parameters or model version. Please verify your settings.';
|
: 'Google Gemini API call failed.';
|
||||||
} else if (status === 403) {
|
|
||||||
|
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.';
|
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.';
|
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);
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1013,11 +1013,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="font-size: 0.85rem; line-height: 1.4; color: var(--text-muted); margin-bottom: 16px;">
|
<p style="font-size: 0.85rem; line-height: 1.4; color: var(--text-muted); margin-bottom: 16px;">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form id="settings-api-form" style="margin-bottom: 8px;">
|
<form id="settings-api-form" style="margin-bottom: 8px;">
|
||||||
<div class="form-group mb-4">
|
<div class="form-group mb-3">
|
||||||
<label for="settings-gemini-key" style="font-size: 0.85rem; margin-bottom: 6px; display: block; font-weight: 500;">Google Gemini API Key</label>
|
<label for="settings-gemini-key" style="font-size: 0.85rem; margin-bottom: 6px; display: block; font-weight: 500;">Google Gemini API Key</label>
|
||||||
<div class="input-wrapper" style="position: relative; display: flex; align-items: center;">
|
<div class="input-wrapper" style="position: relative; display: flex; align-items: center;">
|
||||||
<input type="password" id="settings-gemini-key" placeholder="Enter your Gemini API key..." autocomplete="off" style="width: 100%; padding-right: 40px; height: 40px; border-radius: var(--border-radius-md); border: 1px solid var(--border-color); background: rgba(0,0,0,0.2); color: var(--text-main); padding-left: 12px;">
|
<input type="password" id="settings-gemini-key" placeholder="Enter your Gemini API key..." autocomplete="off" style="width: 100%; padding-right: 40px; height: 40px; border-radius: var(--border-radius-md); border: 1px solid var(--border-color); background: rgba(0,0,0,0.2); color: var(--text-main); padding-left: 12px;">
|
||||||
|
|
@ -1026,8 +1026,25 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||||
|
<label for="settings-gemini-model" style="font-size: 0.85rem; display: block; font-weight: 500;">AI Model</label>
|
||||||
|
<button type="button" id="btn-discover-models" class="btn btn-secondary btn-sm" style="font-size: 0.75rem; padding: 2px 10px; height: 28px; display: inline-flex; align-items: center; gap: 5px; cursor: pointer;" title="Query Google API for available models on your key">
|
||||||
|
<i data-lucide="refresh-cw" style="width: 13px; height: 13px;"></i>
|
||||||
|
<span>Discover Models</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select id="settings-gemini-model" style="width: 100%; height: 40px; border-radius: var(--border-radius-md); border: 1px solid var(--border-color); background: var(--bg-card, #1e2430); color: var(--text-main); padding-left: 12px; padding-right: 12px; font-size: 0.85rem; cursor: pointer;">
|
||||||
|
<option value="gemini-2.0-flash">Gemini 2.0 Flash (Recommended)</option>
|
||||||
|
<option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
|
||||||
|
<option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
|
||||||
|
</select>
|
||||||
|
<span id="models-status-hint" style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px; display: block;">Powers both recipe photo scanning and AI text polishing.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" style="width: 100%; height: 40px;">
|
<button type="submit" class="btn btn-primary" style="width: 100%; height: 40px;">
|
||||||
<i data-lucide="save"></i> <span>Save Credentials</span>
|
<i data-lucide="save"></i> <span>Save Settings</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"geminiApiKey": "PASTE_YOUR_GEMINI_API_KEY_HERE"
|
"geminiApiKey": "PASTE_YOUR_GEMINI_API_KEY_HERE",
|
||||||
|
"geminiModel": "gemini-2.0-flash"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
76
server.js
76
server.js
|
|
@ -80,7 +80,8 @@ db.prepare(`
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
geminiApiKey TEXT DEFAULT ''
|
geminiApiKey TEXT DEFAULT '',
|
||||||
|
geminiModel TEXT DEFAULT 'gemini-2.0-flash'
|
||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
|
|
@ -190,11 +191,17 @@ try {
|
||||||
try {
|
try {
|
||||||
db.prepare("ALTER TABLE backup_history ADD COLUMN userSummary TEXT NOT NULL DEFAULT '[]'").run();
|
db.prepare("ALTER TABLE backup_history ADD COLUMN userSummary TEXT NOT NULL DEFAULT '[]'").run();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
db.prepare("ALTER TABLE settings ADD COLUMN geminiModel TEXT DEFAULT 'gemini-2.0-flash'").run();
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
db.prepare("UPDATE settings SET geminiModel = 'gemini-2.0-flash' WHERE geminiModel IS NULL OR geminiModel = ''").run();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
// Initialize single settings record if empty
|
// Initialize single settings record if empty
|
||||||
const settingsCount = db.prepare('SELECT COUNT(*) as count FROM settings').get().count;
|
const settingsCount = db.prepare('SELECT COUNT(*) as count FROM settings').get().count;
|
||||||
if (settingsCount === 0) {
|
if (settingsCount === 0) {
|
||||||
db.prepare('INSERT INTO settings (geminiApiKey) VALUES (?)').run('');
|
db.prepare('INSERT INTO settings (geminiApiKey, geminiModel) VALUES (?, ?)').run('', 'gemini-2.0-flash');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure default system_admin and admin user profiles exist
|
// Ensure default system_admin and admin user profiles exist
|
||||||
|
|
@ -1575,8 +1582,9 @@ async function scrapeTikTokRecipe(inputUrl) {
|
||||||
app.post('/api/recipes/ai-polish', requireAuth, async (req, res) => {
|
app.post('/api/recipes/ai-polish', requireAuth, async (req, res) => {
|
||||||
const { rawCaption, title, ingredients, instructions } = req.body;
|
const { rawCaption, title, ingredients, instructions } = req.body;
|
||||||
|
|
||||||
const settingsRow = db.prepare('SELECT geminiApiKey FROM settings LIMIT 1').get();
|
const settingsRow = db.prepare('SELECT geminiApiKey, geminiModel FROM settings LIMIT 1').get();
|
||||||
const geminiKey = settingsRow?.geminiApiKey?.trim();
|
const geminiKey = settingsRow?.geminiApiKey?.trim();
|
||||||
|
const geminiModel = settingsRow?.geminiModel?.trim() || 'gemini-2.0-flash';
|
||||||
const isPlaceholder = !geminiKey || geminiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE';
|
const isPlaceholder = !geminiKey || geminiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE';
|
||||||
|
|
||||||
if (isPlaceholder) {
|
if (isPlaceholder) {
|
||||||
|
|
@ -1612,7 +1620,7 @@ Return ONLY a valid JSON object matching this schema without markdown code block
|
||||||
"instructions": ["Step 1", "Step 2"]
|
"instructions": ["Step 1", "Step 2"]
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiKey}`;
|
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${geminiKey}`;
|
||||||
const aiRes = await axios.post(geminiUrl, {
|
const aiRes = await axios.post(geminiUrl, {
|
||||||
contents: [{ parts: [{ text: promptText }] }],
|
contents: [{ parts: [{ text: promptText }] }],
|
||||||
generationConfig: { responseMimeType: "application/json" }
|
generationConfig: { responseMimeType: "application/json" }
|
||||||
|
|
@ -1638,11 +1646,12 @@ Return ONLY a valid JSON object matching this schema without markdown code block
|
||||||
// API: Load System Settings & User Preferences
|
// API: Load System Settings & User Preferences
|
||||||
app.get('/api/settings', requireAuth, (req, res) => {
|
app.get('/api/settings', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const settings = db.prepare('SELECT geminiApiKey FROM settings LIMIT 1').get();
|
const settings = db.prepare('SELECT geminiApiKey, geminiModel FROM settings LIMIT 1').get();
|
||||||
const userRow = db.prepare('SELECT defaultConsolidateLists FROM users WHERE id = ?').get(req.user.id);
|
const userRow = db.prepare('SELECT defaultConsolidateLists FROM users WHERE id = ?').get(req.user.id);
|
||||||
const defaultConsolidateLists = userRow ? userRow.defaultConsolidateLists !== 0 : true;
|
const defaultConsolidateLists = userRow ? userRow.defaultConsolidateLists !== 0 : true;
|
||||||
res.json({
|
res.json({
|
||||||
geminiApiKey: settings ? settings.geminiApiKey : '',
|
geminiApiKey: settings ? settings.geminiApiKey : '',
|
||||||
|
geminiModel: (settings && settings.geminiModel) ? settings.geminiModel : 'gemini-2.0-flash',
|
||||||
defaultConsolidateLists
|
defaultConsolidateLists
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -1653,19 +1662,23 @@ app.get('/api/settings', requireAuth, (req, res) => {
|
||||||
|
|
||||||
// API: Save System Settings & User Preferences
|
// API: Save System Settings & User Preferences
|
||||||
app.post('/api/settings', requireAuth, (req, res) => {
|
app.post('/api/settings', requireAuth, (req, res) => {
|
||||||
const { geminiApiKey, defaultConsolidateLists } = req.body;
|
const { geminiApiKey, geminiModel, defaultConsolidateLists } = req.body;
|
||||||
try {
|
try {
|
||||||
if (geminiApiKey !== undefined) {
|
if (geminiApiKey !== undefined) {
|
||||||
db.prepare('UPDATE settings SET geminiApiKey = ?').run(geminiApiKey || '');
|
db.prepare('UPDATE settings SET geminiApiKey = ?').run(geminiApiKey || '');
|
||||||
}
|
}
|
||||||
|
if (geminiModel !== undefined) {
|
||||||
|
db.prepare('UPDATE settings SET geminiModel = ?').run(geminiModel || 'gemini-2.0-flash');
|
||||||
|
}
|
||||||
if (typeof defaultConsolidateLists === 'boolean') {
|
if (typeof defaultConsolidateLists === 'boolean') {
|
||||||
db.prepare('UPDATE users SET defaultConsolidateLists = ? WHERE id = ?').run(defaultConsolidateLists ? 1 : 0, req.user.id);
|
db.prepare('UPDATE users SET defaultConsolidateLists = ? WHERE id = ?').run(defaultConsolidateLists ? 1 : 0, req.user.id);
|
||||||
req.user.defaultConsolidateLists = defaultConsolidateLists;
|
req.user.defaultConsolidateLists = defaultConsolidateLists;
|
||||||
}
|
}
|
||||||
const settings = db.prepare('SELECT geminiApiKey FROM settings LIMIT 1').get();
|
const settings = db.prepare('SELECT geminiApiKey, geminiModel FROM settings LIMIT 1').get();
|
||||||
const userRow = db.prepare('SELECT defaultConsolidateLists FROM users WHERE id = ?').get(req.user.id);
|
const userRow = db.prepare('SELECT defaultConsolidateLists FROM users WHERE id = ?').get(req.user.id);
|
||||||
res.json({
|
res.json({
|
||||||
geminiApiKey: settings ? settings.geminiApiKey : '',
|
geminiApiKey: settings ? settings.geminiApiKey : '',
|
||||||
|
geminiModel: (settings && settings.geminiModel) ? settings.geminiModel : 'gemini-2.0-flash',
|
||||||
defaultConsolidateLists: userRow ? userRow.defaultConsolidateLists !== 0 : true
|
defaultConsolidateLists: userRow ? userRow.defaultConsolidateLists !== 0 : true
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -1674,6 +1687,55 @@ app.post('/api/settings', requireAuth, (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API: Discover available Gemini models using the saved or provided API key
|
||||||
|
app.post('/api/settings/discover-models', requireAuth, async (req, res) => {
|
||||||
|
let { apiKey } = req.body || {};
|
||||||
|
if (!apiKey) {
|
||||||
|
const settingsRow = db.prepare('SELECT geminiApiKey FROM settings LIMIT 1').get();
|
||||||
|
apiKey = settingsRow?.geminiApiKey?.trim();
|
||||||
|
}
|
||||||
|
if (!apiKey || apiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE') {
|
||||||
|
return res.status(400).json({ error: 'Please enter a Gemini API Key first.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, {
|
||||||
|
timeout: 12000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && Array.isArray(response.data.models)) {
|
||||||
|
const models = response.data.models
|
||||||
|
.filter(m => Array.isArray(m.supportedGenerationMethods) && m.supportedGenerationMethods.includes('generateContent'))
|
||||||
|
.filter(m => !m.name.includes('embedding') && !m.name.includes('aqa') && !m.name.includes('imagen'))
|
||||||
|
.map(m => {
|
||||||
|
const modelId = m.name.replace(/^models\//, '');
|
||||||
|
return {
|
||||||
|
id: modelId,
|
||||||
|
displayName: m.displayName || modelId,
|
||||||
|
description: m.description || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort models: Flash models first, then by ID
|
||||||
|
models.sort((a, b) => {
|
||||||
|
const aIsFlash = a.id.toLowerCase().includes('flash');
|
||||||
|
const bIsFlash = b.id.toLowerCase().includes('flash');
|
||||||
|
if (aIsFlash && !bIsFlash) return -1;
|
||||||
|
if (!aIsFlash && bIsFlash) return 1;
|
||||||
|
return a.id.localeCompare(b.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ models });
|
||||||
|
} else {
|
||||||
|
return res.status(500).json({ error: 'Unexpected response from Google Gemini API.' });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Model discovery error:', err.response?.data?.error?.message || err.message);
|
||||||
|
const msg = err.response?.data?.error?.message || err.message;
|
||||||
|
return res.status(err.response?.status || 500).json({ error: `Failed to fetch models: ${msg}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// API: Update User Preferences directly
|
// API: Update User Preferences directly
|
||||||
app.put('/api/user/preferences', requireAuth, (req, res) => {
|
app.put('/api/user/preferences', requireAuth, (req, res) => {
|
||||||
const { defaultConsolidateLists } = req.body;
|
const { defaultConsolidateLists } = req.body;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue