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: = json_encode($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: = json_encode($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']];