diff --git a/brainstorm_canvas.php b/brainstorm_canvas.php index b191409..f01ebd5 100644 --- a/brainstorm_canvas.php +++ b/brainstorm_canvas.php @@ -1421,57 +1421,87 @@ async function aiSuggestRoadmapMeta(btn) { if (!firstContent) return; const origHtml = btn ? btn.innerHTML : ''; - if (btn) { - btn.innerHTML = 'Analyseren...'; - btn.disabled = true; - if (typeof lucide !== 'undefined') lucide.createIcons(); - } + let attempt = 0; + const maxAttempts = 3; - try { - const res = await fetch('/api/ai.php?action=summarize_to_roadmap', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - content: firstContent, - session_title: - }) - }); - const data = await res.json(); - if (data.success && data.item) { - if (data.item.quarter && document.getElementById('roadmapQuarter')) { - document.getElementById('roadmapQuarter').value = data.item.quarter; + const executeRequest = async () => { + attempt++; + if (btn) { + btn.innerHTML = `${attempt > 1 ? `Wachtrij (${attempt}/${maxAttempts})...` : 'Analyseren...'}`; + btn.disabled = true; + if (typeof lucide !== 'undefined') lucide.createIcons(); + } + + try { + const res = await fetch('/api/ai.php?action=summarize_to_roadmap', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + content: firstContent, + session_title: + }) + }); + const data = await res.json(); + if (data.success && data.item) { + if (data.item.quarter && document.getElementById('roadmapQuarter')) { + document.getElementById('roadmapQuarter').value = data.item.quarter; + } + if (data.item.type && document.getElementById('roadmapType')) { + document.getElementById('roadmapType').value = data.item.type; + } + if (data.item.priority && document.getElementById('roadmapPriority')) { + document.getElementById('roadmapPriority').value = data.item.priority; + } + if (btn) { + btn.innerHTML = '' + (data.is_fallback ? 'Ingevuld (Standaard)' : 'Ingevuld!') + ''; + if (typeof lucide !== 'undefined') lucide.createIcons(); + setTimeout(() => { + btn.innerHTML = origHtml; + btn.disabled = false; + if (typeof lucide !== 'undefined') lucide.createIcons(); + }, 1800); + } + } else { + const err = (data.error || '').toLowerCase(); + const isOverloaded = err.includes('demand') || err.includes('druk') || err.includes('spikes') || err.includes('busy') || err.includes('temporarily') || err.includes('quota'); + + if (attempt < maxAttempts && isOverloaded) { + if (btn) { + btn.innerHTML = `Piekdrukte, wachtrij...`; + if (typeof lucide !== 'undefined') lucide.createIcons(); + } + setTimeout(executeRequest, 2000); + return; + } + + // Fallback: vul standaardwaarden in zodat het omzetten altijd kan doorgaan + if (document.getElementById('roadmapQuarter') && !document.getElementById('roadmapQuarter').value) { + document.getElementById('roadmapQuarter').value = 'Q3'; + } + if (btn) { + btn.innerHTML = 'Ingevuld (Standaard)'; + if (typeof lucide !== 'undefined') lucide.createIcons(); + setTimeout(() => { + btn.innerHTML = origHtml; + btn.disabled = false; + if (typeof lucide !== 'undefined') lucide.createIcons(); + }, 1800); + } } - if (data.item.type && document.getElementById('roadmapType')) { - document.getElementById('roadmapType').value = data.item.type; - } - if (data.item.priority && document.getElementById('roadmapPriority')) { - document.getElementById('roadmapPriority').value = data.item.priority; - } - if (btn) { - btn.innerHTML = 'Ingevuld!'; - if (typeof lucide !== 'undefined') lucide.createIcons(); - setTimeout(() => { + } catch(err) { + if (attempt < maxAttempts) { + setTimeout(executeRequest, 2000); + } else { + if (btn) { btn.innerHTML = origHtml; btn.disabled = false; if (typeof lucide !== 'undefined') lucide.createIcons(); - }, 1800); - } - } else { - alert(data.error || 'Kon geen AI advies ophalen'); - if (btn) { - btn.innerHTML = origHtml; - btn.disabled = false; - if (typeof lucide !== 'undefined') lucide.createIcons(); + } } } - } catch(err) { - alert('Netwerkfout bij aanroepen van AI'); - if (btn) { - btn.innerHTML = origHtml; - btn.disabled = false; - if (typeof lucide !== 'undefined') lucide.createIcons(); - } - } + }; + + await executeRequest(); } // ========================================== @@ -1535,7 +1565,16 @@ async function regenerateBreakdown() { } } -async function fetchAiBreakdown(content) { +async function fetchAiBreakdown(content, attempt = 1, maxAttempts = 3) { + const loadingEl = document.getElementById('breakdownLoading'); + const loadingText = loadingEl ? loadingEl.querySelector('p') : null; + + if (attempt > 1 && loadingText) { + loadingText.innerHTML = `⏳ AI Model heeft piekdruk (high demand).
Aanvraag staat in de wachtrij en wordt opnieuw geprobeerd (poging ${attempt}/${maxAttempts})...`; + } else if (loadingText) { + loadingText.innerHTML = 'AI ontleedt het idee in behapbare taken...'; + } + try { const res = await fetch('/api/ai.php?action=breakdown_idea', { method: 'POST', @@ -1551,6 +1590,14 @@ async function fetchAiBreakdown(content) { const data = await res.json(); if (!data.success || !data.items || data.items.length === 0) { + const err = (data.error || '').toLowerCase(); + const isOverloaded = err.includes('demand') || err.includes('druk') || err.includes('spikes') || err.includes('busy') || err.includes('temporarily') || err.includes('quota'); + + if (attempt < maxAttempts && isOverloaded) { + setTimeout(() => fetchAiBreakdown(content, attempt + 1, maxAttempts), 2500); + return; + } + document.getElementById('breakdownLoading').classList.add('hidden'); document.getElementById('breakdownError').classList.remove('hidden'); document.getElementById('breakdownErrorMsg').innerText = data.error || 'Geen taken gegenereerd door AI.'; @@ -1576,6 +1623,10 @@ async function fetchAiBreakdown(content) { document.getElementById('breakdownResult').classList.remove('hidden'); if (window.lucide) lucide.createIcons(); } catch (err) { + if (attempt < maxAttempts) { + setTimeout(() => fetchAiBreakdown(content, attempt + 1, maxAttempts), 2500); + return; + } document.getElementById('breakdownLoading').classList.add('hidden'); document.getElementById('breakdownError').classList.remove('hidden'); document.getElementById('breakdownErrorMsg').innerText = 'Netwerkfout bij communicatie met AI: ' + err.message; diff --git a/includes/ai.php b/includes/ai.php index 91af02e..2db975d 100644 --- a/includes/ai.php +++ b/includes/ai.php @@ -185,6 +185,23 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings $addCandidatesFor('gemini-1.5-pro'); $addCandidatesFor('gemini-2.5-flash'); + $isHighDemandOrRateLimited = function(array $res, string $msg): bool { + $status = $res['status'] ?? 0; + if ($status === 503 || $status === 429) { + return true; + } + $lower = strtolower($msg); + return ( + str_contains($lower, 'high demand') || + str_contains($lower, 'spikes in demand') || + str_contains($lower, 'temporarily unavailable') || + str_contains($lower, 'resource has been exhausted') || + str_contains($lower, 'rate limit') || + str_contains($lower, 'overloaded') || + str_contains($lower, 'too many requests') + ); + }; + $response = null; $lastError = null; $successfulModel = $cleanModel; @@ -193,31 +210,42 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings for ($i = 0; $i < count($candidateUrls); $i++) { $baseUrl = $candidateUrls[$i]; $endpoint = $baseUrl . '?key=' . urlencode($apiKey); - $res = fociz_http_request($endpoint, [ - 'method' => 'POST', - 'headers' => [ - 'Content-Type' => 'application/json', - 'x-goog-api-key' => $apiKey, - ], - 'body' => $payload, - 'timeout' => 20, - ]); - if ($res['status'] === 200 && !empty($res['json']['candidates'][0])) { - $extracted = $extractCandidateText($res['json']['candidates'][0]); - // Als er tekst is, of finishReason is geldig (bijv. STOP of MAX_TOKENS) - if ($extracted !== '' || !empty($res['json']['candidates'][0]['finishReason'])) { - $response = $res; - $finalText = ($extracted !== '') ? $extracted : 'OK'; - if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) { - $successfulModel = $mName[1]; + // Doe max 2 pogingen met korte micro-backoff bij een tijdelijke piek (transient spike) + for ($attempt = 1; $attempt <= 2; $attempt++) { + $res = fociz_http_request($endpoint, [ + 'method' => 'POST', + 'headers' => [ + 'Content-Type' => 'application/json', + 'x-goog-api-key' => $apiKey, + ], + 'body' => $payload, + 'timeout' => 25, + ]); + + if ($res['status'] === 200 && !empty($res['json']['candidates'][0])) { + $extracted = $extractCandidateText($res['json']['candidates'][0]); + // Als er tekst is, of finishReason is geldig (bijv. STOP of MAX_TOKENS) + if ($extracted !== '' || !empty($res['json']['candidates'][0]['finishReason'])) { + $response = $res; + $finalText = ($extracted !== '') ? $extracted : 'OK'; + if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) { + $successfulModel = $mName[1]; + } + break 2; // Volledig succes, beëindig beide loops } - break; } - } - $msg = $res['json']['error']['message'] ?? ($res['error'] ?? ''); - $lastError = $msg; + $msg = $res['json']['error']['message'] ?? ($res['error'] ?? ''); + $lastError = $msg; + + // Als dit een tijdelijke piek is en we nog een poging over hebben, wacht kort en probeer nogmaals + if ($attempt < 2 && $isHighDemandOrRateLimited($res, $msg)) { + usleep(800000); // 800ms + continue; + } + break; + } // Als Google in de foutmelding direct een nieuwer model aanbeveelt (bv. "use models/gemini-3.6-flash") if (preg_match('#use (?:models/)?(gemini-[a-zA-Z0-9\.\-_]+)#i', $msg, $recMatch)) { @@ -228,11 +256,18 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings } } - // Als het model niet gevonden is, niet meer beschikbaar is of deprecated is, probeer de volgende kandidaat + // 1. Model niet gevonden of deprecated -> probeer de volgende kandidaat if ($res['status'] === 404 || str_contains($msg, 'not found') || str_contains($msg, 'not supported') || str_contains($msg, 'no longer available') || str_contains($msg, 'deprecated')) { continue; } + // 2. HIGH DEMAND / 503 / 429 OVERBELASTING: NIET STOPPEN! + // Schakel direct door naar de volgende fallback kandidaat (bijv. snellere flash-modellen) + if ($isHighDemandOrRateLimited($res, $msg)) { + error_log("fociz_ai_chat_gemini: Model op {$baseUrl} heeft piekdruk (high demand: '{$msg}'). Schakelt automatisch door naar alternatief model..."); + continue; + } + // Bij autorisatiefouten (zoals ongeldige API key 400/403) direct stoppen $response = $res; break; @@ -549,9 +584,39 @@ function fociz_ai_breakdown_idea(string $ideaContent, string $sessionTitle = '', if (!$aiRes['success']) { error_log("[fociz_ai_breakdown_idea] AI request failed: " . ($aiRes['error'] ?? 'Unknown error')); + // Fallback: Als AI na alle modelpogingen niet reageert, creëer 3 logische taken zodat de workflow direct doorloopt + $shortTitle = mb_substr(explode("\n", trim($ideaContent))[0], 0, 60); return [ - 'success' => false, - 'error' => $aiRes['error'] ?? 'Kon het idee niet ontleden in taken via AI.', + 'success' => true, + 'summary' => "Dit idee is direct voorbereid in behapbare deeltaken.", + 'items' => [ + [ + 'title' => "Onderzoek & specificatie: {$shortTitle}", + 'description' => "Randvoorwaarden en specificaties vastleggen voor: {$ideaContent}", + 'estimated_minutes' => 45, + 'priority' => 'P1', + 'color' => 'indigo', + 'category' => 'Onderzoek' + ], + [ + 'title' => "Realisatie & uitwerking: {$shortTitle}", + 'description' => "Implementatie en concrete uitvoering van het idee.", + 'estimated_minutes' => 90, + 'priority' => 'P2', + 'color' => 'blue', + 'category' => 'Frontend' + ], + [ + 'title' => "Kwaliteitscontrole & oplevering: {$shortTitle}", + 'description' => "Testen, feedback verwerken en afronden.", + 'estimated_minutes' => 30, + 'priority' => 'P2', + 'color' => 'emerald', + 'category' => 'Testing' + ] + ], + 'model' => 'Smart Fallback', + 'is_fallback' => true, ]; } @@ -726,7 +791,25 @@ function fociz_ai_summarize_to_roadmap(string $content, string $sessionTitle = ' ], ['json_mode' => true, 'user_id' => $userId]); if (!$aiRes['success'] || empty($aiRes['json']['title'])) { - return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon roadmap item niet formuleren']; + // Intelligente fallback bij storing / high demand zodat roadmap conversie nooit faalt + $lines = explode("\n", trim($content)); + $firstLine = trim($lines[0] ?? 'Brainstorm Initiatief'); + $cleanTitle = mb_substr($firstLine, 0, 75); + $currentMonth = (int)date('n'); + $q = 'Q' . ceil($currentMonth / 3); + + return [ + 'success' => true, + 'item' => [ + 'title' => $cleanTitle, + 'description' => $content, + 'priority' => 'HIGH', + 'type' => 'FEATURE', + 'quarter' => $q, + ], + 'is_fallback' => true, + 'notice' => 'AI model had tijdelijke piekdruk; metadata automatisch ingevuld.' + ]; } return ['success' => true, 'item' => $aiRes['json']];