Compare commits
2 Commits
660f196c60
...
f18f7d5794
| Author | SHA1 | Date | |
|---|---|---|---|
| f18f7d5794 | |||
| 45600040ea |
+96
-45
@@ -1421,57 +1421,87 @@ async function aiSuggestRoadmapMeta(btn) {
|
|||||||
if (!firstContent) return;
|
if (!firstContent) return;
|
||||||
|
|
||||||
const origHtml = btn ? btn.innerHTML : '';
|
const origHtml = btn ? btn.innerHTML : '';
|
||||||
if (btn) {
|
let attempt = 0;
|
||||||
btn.innerHTML = '<span class="animate-pulse flex items-center space-x-1"><i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Analyseren...</span></span>';
|
const maxAttempts = 3;
|
||||||
btn.disabled = true;
|
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const executeRequest = async () => {
|
||||||
const res = await fetch('/api/ai.php?action=summarize_to_roadmap', {
|
attempt++;
|
||||||
method: 'POST',
|
if (btn) {
|
||||||
headers: {'Content-Type': 'application/json'},
|
btn.innerHTML = `<span class="animate-pulse flex items-center space-x-1"><i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>${attempt > 1 ? `Wachtrij (${attempt}/${maxAttempts})...` : 'Analyseren...'}</span></span>`;
|
||||||
body: JSON.stringify({
|
btn.disabled = true;
|
||||||
content: firstContent,
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||||
session_title: <?= json_encode($session['title']) ?>
|
}
|
||||||
})
|
|
||||||
});
|
try {
|
||||||
const data = await res.json();
|
const res = await fetch('/api/ai.php?action=summarize_to_roadmap', {
|
||||||
if (data.success && data.item) {
|
method: 'POST',
|
||||||
if (data.item.quarter && document.getElementById('roadmapQuarter')) {
|
headers: {'Content-Type': 'application/json'},
|
||||||
document.getElementById('roadmapQuarter').value = data.item.quarter;
|
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 = '<i data-lucide="check" class="w-3.5 h-3.5"></i><span>' + (data.is_fallback ? 'Ingevuld (Standaard)' : 'Ingevuld!') + '</span>';
|
||||||
|
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 = `<span class="animate-pulse flex items-center space-x-1"><i data-lucide="clock" class="w-3.5 h-3.5"></i><span>Piekdrukte, wachtrij...</span></span>`;
|
||||||
|
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 = '<i data-lucide="check" class="w-3.5 h-3.5"></i><span>Ingevuld (Standaard)</span>';
|
||||||
|
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')) {
|
} catch(err) {
|
||||||
document.getElementById('roadmapType').value = data.item.type;
|
if (attempt < maxAttempts) {
|
||||||
}
|
setTimeout(executeRequest, 2000);
|
||||||
if (data.item.priority && document.getElementById('roadmapPriority')) {
|
} else {
|
||||||
document.getElementById('roadmapPriority').value = data.item.priority;
|
if (btn) {
|
||||||
}
|
|
||||||
if (btn) {
|
|
||||||
btn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5"></i><span>Ingevuld!</span>';
|
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
||||||
setTimeout(() => {
|
|
||||||
btn.innerHTML = origHtml;
|
btn.innerHTML = origHtml;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
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) {
|
await executeRequest();
|
||||||
btn.innerHTML = origHtml;
|
|
||||||
btn.disabled = false;
|
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -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 = `<span class="font-bold text-amber-500">⏳ AI Model heeft piekdruk (high demand).</span><br><span class="text-xs text-slate-400">Aanvraag staat in de wachtrij en wordt opnieuw geprobeerd (poging ${attempt}/${maxAttempts})...</span>`;
|
||||||
|
} else if (loadingText) {
|
||||||
|
loadingText.innerHTML = 'AI ontleedt het idee in behapbare taken...';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/ai.php?action=breakdown_idea', {
|
const res = await fetch('/api/ai.php?action=breakdown_idea', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1551,6 +1590,14 @@ async function fetchAiBreakdown(content) {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (!data.success || !data.items || data.items.length === 0) {
|
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('breakdownLoading').classList.add('hidden');
|
||||||
document.getElementById('breakdownError').classList.remove('hidden');
|
document.getElementById('breakdownError').classList.remove('hidden');
|
||||||
document.getElementById('breakdownErrorMsg').innerText = data.error || 'Geen taken gegenereerd door AI.';
|
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');
|
document.getElementById('breakdownResult').classList.remove('hidden');
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (attempt < maxAttempts) {
|
||||||
|
setTimeout(() => fetchAiBreakdown(content, attempt + 1, maxAttempts), 2500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.getElementById('breakdownLoading').classList.add('hidden');
|
document.getElementById('breakdownLoading').classList.add('hidden');
|
||||||
document.getElementById('breakdownError').classList.remove('hidden');
|
document.getElementById('breakdownError').classList.remove('hidden');
|
||||||
document.getElementById('breakdownErrorMsg').innerText = 'Netwerkfout bij communicatie met AI: ' + err.message;
|
document.getElementById('breakdownErrorMsg').innerText = 'Netwerkfout bij communicatie met AI: ' + err.message;
|
||||||
|
|||||||
+108
-25
@@ -185,6 +185,23 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
$addCandidatesFor('gemini-1.5-pro');
|
$addCandidatesFor('gemini-1.5-pro');
|
||||||
$addCandidatesFor('gemini-2.5-flash');
|
$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;
|
$response = null;
|
||||||
$lastError = null;
|
$lastError = null;
|
||||||
$successfulModel = $cleanModel;
|
$successfulModel = $cleanModel;
|
||||||
@@ -193,31 +210,42 @@ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings
|
|||||||
for ($i = 0; $i < count($candidateUrls); $i++) {
|
for ($i = 0; $i < count($candidateUrls); $i++) {
|
||||||
$baseUrl = $candidateUrls[$i];
|
$baseUrl = $candidateUrls[$i];
|
||||||
$endpoint = $baseUrl . '?key=' . urlencode($apiKey);
|
$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])) {
|
// Doe max 2 pogingen met korte micro-backoff bij een tijdelijke piek (transient spike)
|
||||||
$extracted = $extractCandidateText($res['json']['candidates'][0]);
|
for ($attempt = 1; $attempt <= 2; $attempt++) {
|
||||||
// Als er tekst is, of finishReason is geldig (bijv. STOP of MAX_TOKENS)
|
$res = fociz_http_request($endpoint, [
|
||||||
if ($extracted !== '' || !empty($res['json']['candidates'][0]['finishReason'])) {
|
'method' => 'POST',
|
||||||
$response = $res;
|
'headers' => [
|
||||||
$finalText = ($extracted !== '') ? $extracted : 'OK';
|
'Content-Type' => 'application/json',
|
||||||
if (preg_match('#/models/([^:]+)#', $baseUrl, $mName)) {
|
'x-goog-api-key' => $apiKey,
|
||||||
$successfulModel = $mName[1];
|
],
|
||||||
|
'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'] ?? '');
|
$msg = $res['json']['error']['message'] ?? ($res['error'] ?? '');
|
||||||
$lastError = $msg;
|
$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")
|
// 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)) {
|
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')) {
|
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;
|
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
|
// Bij autorisatiefouten (zoals ongeldige API key 400/403) direct stoppen
|
||||||
$response = $res;
|
$response = $res;
|
||||||
break;
|
break;
|
||||||
@@ -549,9 +584,39 @@ function fociz_ai_breakdown_idea(string $ideaContent, string $sessionTitle = '',
|
|||||||
|
|
||||||
if (!$aiRes['success']) {
|
if (!$aiRes['success']) {
|
||||||
error_log("[fociz_ai_breakdown_idea] AI request failed: " . ($aiRes['error'] ?? 'Unknown error'));
|
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 [
|
return [
|
||||||
'success' => false,
|
'success' => true,
|
||||||
'error' => $aiRes['error'] ?? 'Kon het idee niet ontleden in taken via AI.',
|
'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]);
|
], ['json_mode' => true, 'user_id' => $userId]);
|
||||||
|
|
||||||
if (!$aiRes['success'] || empty($aiRes['json']['title'])) {
|
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']];
|
return ['success' => true, 'item' => $aiRes['json']];
|
||||||
|
|||||||
Reference in New Issue
Block a user