0, 'jira_urls' => 0, 'teams_links' => 0, 'email_addrs' => 0, 'mail_headers' => 0, ]; $patterns = [ // Jira ticket keys zoals PROJ-1234, ABC-99 (2 tot 10 hoofdletters gevolgd door streepje en cijfers) 'jira_tickets' => '/\b[A-Z]{2,10}-\d+\b/', // Jira / Atlassian domeinen en URLs 'jira_urls' => '/https?:\/\/[a-zA-Z0-9-]+\.atlassian\.net[^\s]*/i', // Teams URLs en links 'teams_links' => '/https?:\/\/teams\.microsoft\.com[^\s]*/i', // E-mailadressen 'email_addrs' => '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', // E-mailheaders en Microsoft Outlook / Teams traces 'mail_headers' => '/\b(Subject:|Onderwerp:|From:|Van:|To:|Aan:|CC:|BCC:|FW:|Fwd:|RE:)\s*[^\r\n]+/i', ]; $replacements = [ 'jira_tickets' => '[TICKET_AFGESCHERMD]', 'jira_urls' => '[JIRA_URL_AFGESCHERMD]', 'teams_links' => '[TEAMS_LINK_AFGESCHERMD]', 'email_addrs' => '[EMAIL_AFGESCHERMD]', 'mail_headers' => '[MAIL_HEADER_AFGESCHERMD]', ]; foreach ($messages as $idx => $msg) { if (!isset($msg['content']) || !is_string($msg['content'])) { continue; } $text = $msg['content']; foreach ($patterns as $key => $pattern) { $count = 0; $text = preg_replace($pattern, $replacements[$key], $text, -1, $count); if ($count > 0) { $detections[$key] += $count; } } $messages[$idx]['content'] = $text; } $totalBlocked = array_sum($detections); if ($totalBlocked > 0) { error_log(sprintf( "[FOCIZ AI FIREWALL] Zero-Data Leakage actief: %d potentiële datalekken geblokkeerd en geanonimiseerd (%s)", $totalBlocked, json_encode($detections) )); } return $detections; } /** * Voert een gestructureerde chat completion uit met Google Gemini Pro * * @param array $messages * @param array $options * @param array $aiSettings * @return array */ function fociz_ai_chat_gemini(array $messages, array $options, array $aiSettings): array { $apiKey = trim((string)($aiSettings['ai_api_key'] ?? '')); $model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gemini-2.0-flash'; $temperature = $options['temperature'] ?? 0.3; $maxTokens = max(512, (int)($options['max_tokens'] ?? 2048)); $jsonMode = !empty($options['json_mode']); if (empty($apiKey)) { return [ 'success' => false, 'error' => 'Geen Google Gemini API-sleutel ingevuld of geconfigureerd.', ]; } // Google Generative Language API structuur opbouwen $systemInstructions = []; $contents = []; foreach ($messages as $msg) { $role = $msg['role'] ?? 'user'; $text = (string)($msg['content'] ?? ''); if ($role === 'system') { $systemInstructions[] = ['text' => $text]; } else { $contents[] = [ 'role' => ($role === 'assistant') ? 'model' : 'user', 'parts' => [['text' => $text]], ]; } } if (empty($contents)) { $contents[] = [ 'role' => 'user', 'parts' => [['text' => 'Hallo']], ]; } $payload = [ 'contents' => $contents, 'generationConfig' => [ 'temperature' => (float)$temperature, 'maxOutputTokens' => (int)$maxTokens, ], ]; if (!empty($systemInstructions)) { $payload['system_instruction'] = [ 'parts' => $systemInstructions, ]; } if ($jsonMode) { $payload['generationConfig']['responseMimeType'] = 'application/json'; } $cleanModel = preg_replace('#^models/#', '', $model); // Helper om tekst uit Gemini candidate te extraheren $extractCandidateText = function(?array $candidate): string { if (!$candidate || empty($candidate['content']['parts'])) { return ''; } $txt = ''; foreach ($candidate['content']['parts'] as $part) { if (!empty($part['text']) && empty($part['thought'])) { $txt .= $part['text']; } } if ($txt === '' && !empty($candidate['content']['parts'][0]['text'])) { $txt = $candidate['content']['parts'][0]['text']; } return trim($txt); }; // Normaliseer modelnaam en stel geldige kandidatenlijst op $candidateUrls = []; // Helper om endpoints op te bouwen voor v1beta en v1 $addCandidatesFor = function(string $m) use (&$candidateUrls) { $m = preg_replace('#^models/#', '', trim($m)); if ($m === '') return; $v1beta = "https://generativelanguage.googleapis.com/v1beta/models/{$m}:generateContent"; $v1 = "https://generativelanguage.googleapis.com/v1/models/{$m}:generateContent"; if (!in_array($v1beta, $candidateUrls, true)) { $candidateUrls[] = $v1beta; } if (!in_array($v1, $candidateUrls, true)) { $candidateUrls[] = $v1; } }; // Voeg het geconfigureerde model toe als eerste kandidaat (tenzij het een niet-bestaand/fictief model is) if (!empty($cleanModel) && !str_contains($cleanModel, '3.6-flash')) { $addCandidatesFor($cleanModel); } // Voeg altijd de stabiele, snelle Google AI modellen toe als betrouwbare fallbacks/standaarden $addCandidatesFor('gemini-2.0-flash'); $addCandidatesFor('gemini-1.5-flash'); $addCandidatesFor('gemini-1.5-pro'); $addCandidatesFor('gemini-2.5-flash'); $response = null; $lastError = null; $successfulModel = $cleanModel; $finalText = ''; 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]; } break; } } $msg = $res['json']['error']['message'] ?? ($res['error'] ?? ''); $lastError = $msg; // 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)) { $recModel = $recMatch[1]; $recUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$recModel}:generateContent"; if (!in_array($recUrl, $candidateUrls, true)) { $candidateUrls[] = $recUrl; } } // Als het model niet gevonden is, niet meer beschikbaar is of deprecated is, 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; } // Bij autorisatiefouten (zoals ongeldige API key 400/403) direct stoppen $response = $res; break; } // Als alle directe kandidaten een 404 gaven, probeer dynamisch via ListModels een passend gemini model te vinden if (!$response && ($lastError && (str_contains($lastError, 'not found') || str_contains($lastError, 'not supported')))) { $listUrl = "https://generativelanguage.googleapis.com/v1beta/models?key=" . urlencode($apiKey); $listRes = fociz_http_request($listUrl, [ 'method' => 'GET', 'headers' => ['x-goog-api-key' => $apiKey], 'timeout' => 10, ]); if ($listRes['status'] === 200 && !empty($listRes['json']['models'])) { $availModels = []; foreach ($listRes['json']['models'] as $mItem) { $mMethods = $mItem['supportedGenerationMethods'] ?? []; if (in_array('generateContent', $mMethods, true)) { $mClean = preg_replace('#^models/#', '', $mItem['name'] ?? ''); if ($mClean) $availModels[] = $mClean; } } // Zoek een passend gemini model $fallbackModel = null; foreach ($availModels as $am) { if (str_contains($am, 'flash')) { $fallbackModel = $am; break; } } if (!$fallbackModel) { foreach ($availModels as $am) { if (str_contains($am, 'gemini')) { $fallbackModel = $am; break; } } } if ($fallbackModel) { $dynEndpoint = "https://generativelanguage.googleapis.com/v1beta/models/{$fallbackModel}:generateContent?key=" . urlencode($apiKey); $dynRes = fociz_http_request($dynEndpoint, [ 'method' => 'POST', 'headers' => [ 'Content-Type' => 'application/json', 'x-goog-api-key' => $apiKey, ], 'body' => $payload, 'timeout' => 20, ]); if ($dynRes['status'] === 200 && !empty($dynRes['json']['candidates'][0])) { $extracted = $extractCandidateText($dynRes['json']['candidates'][0]); if ($extracted !== '' || !empty($dynRes['json']['candidates'][0]['finishReason'])) { $response = $dynRes; $finalText = ($extracted !== '') ? $extracted : 'OK'; $successfulModel = $fallbackModel; } } else { $response = $dynRes; } } } } if (!$response || $response['status'] !== 200 || $finalText === '') { $errMsg = null; if (!empty($response['json']['error']['message'])) { $errMsg = $response['json']['error']['message']; } elseif (!empty($response['error'])) { $errMsg = $response['error']; } elseif (!empty($response['body'])) { $errMsg = "Gemini fout (HTTP {$response['status']}): " . strip_tags(substr((string)$response['body'], 0, 200)); } else { $errMsg = $lastError ?: "Gemini API aanroep mislukt"; } error_log("fociz_ai_chat_gemini fout: {$errMsg}"); return [ 'success' => false, 'error' => $errMsg, ]; } $rawContent = $finalText; // Indien Gemini het als markdown code block wrapped heeft (bv. ```json ... ```), schoon dit op if (preg_match('/^```(?:json)?\s*(.*?)\s*```$/s', $rawContent, $m)) { $cleanContent = trim($m[1]); } else { $cleanContent = $rawContent; } $parsedJson = null; if ($jsonMode) { $parsedJson = json_decode($cleanContent, true); if ($parsedJson === null && json_last_error() !== JSON_ERROR_NONE) { // Poging 2: json decode met raw $parsedJson = json_decode($rawContent, true); } // Poging 3: extraheer JSON substring {...} of [...] indien AI conversatietekst heeft toegevoegd if ($parsedJson === null) { if (preg_match('/\{(?:[^{}]|(?R))*\}/s', $cleanContent, $matchJson)) { $parsedJson = json_decode($matchJson[0], true); } elseif (preg_match('/\[(?:[^\[\]]|(?R))*\]/s', $cleanContent, $matchJson)) { $parsedJson = json_decode($matchJson[0], true); } } } $promptTokens = (int)($response['json']['usageMetadata']['promptTokenCount'] ?? 0); $completionTokens = (int)($response['json']['usageMetadata']['candidatesTokenCount'] ?? 0); if ($promptTokens === 0 && $completionTokens === 0) { // Schatting obv lengte indien niet geretourneerd $promptTokens = max(1, (int)(strlen(json_encode($payload)) / 4)); $completionTokens = max(1, (int)(strlen($cleanContent) / 4)); } return [ 'success' => true, 'content' => $cleanContent, 'json' => $parsedJson, 'model' => $successfulModel, 'provider' => 'gemini', 'prompt_tokens' => $promptTokens, 'completion_tokens' => $completionTokens, 'total_tokens' => ($promptTokens + $completionTokens), ]; } /** * Voert een gestructureerde chat completion uit met OpenAI * * @param array $messages * @param array $options * @param array $aiSettings * @return array */ function fociz_ai_chat_openai(array $messages, array $options, array $aiSettings): array { $apiKey = trim((string)($aiSettings['ai_api_key'] ?? '')); $model = !empty($aiSettings['ai_model']) ? trim((string)$aiSettings['ai_model']) : 'gpt-4o-mini'; $temperature = $options['temperature'] ?? 0.3; $maxTokens = $options['max_tokens'] ?? 1500; $jsonMode = !empty($options['json_mode']); if (empty($apiKey)) { return [ 'success' => false, 'error' => 'Geen OpenAI API-sleutel ingevuld of geconfigureerd.', ]; } $payload = [ 'model' => $model, 'messages' => $messages, 'temperature' => $temperature, 'max_tokens' => $maxTokens, ]; if ($jsonMode) { $payload['response_format'] = ['type' => 'json_object']; } $response = fociz_http_request('https://api.openai.com/v1/chat/completions', [ 'method' => 'POST', 'headers' => [ 'Authorization' => "Bearer {$apiKey}", 'Content-Type' => 'application/json', ], 'body' => $payload, 'timeout' => 20, ]); if ($response['status'] !== 200 || empty($response['json']['choices'][0]['message']['content'])) { $errMsg = null; if (!empty($response['json']['error']['message'])) { $errMsg = $response['json']['error']['message']; } elseif (!empty($response['error'])) { $errMsg = $response['error']; } elseif (!empty($response['body'])) { $errMsg = "OpenAI fout (HTTP {$response['status']}): " . strip_tags(substr((string)$response['body'], 0, 200)); } else { $errMsg = "OpenAI API aanroep mislukt (HTTP {$response['status']})"; } error_log("fociz_ai_chat_openai fout: {$errMsg}"); return [ 'success' => false, 'error' => $errMsg, ]; } $rawContent = trim($response['json']['choices'][0]['message']['content']); $parsedJson = null; if ($jsonMode) { $parsedJson = json_decode($rawContent, true); } $promptTokens = (int)($response['json']['usage']['prompt_tokens'] ?? 0); $completionTokens = (int)($response['json']['usage']['completion_tokens'] ?? 0); if ($promptTokens === 0 && $completionTokens === 0) { $promptTokens = max(1, (int)(strlen(json_encode($payload)) / 4)); $completionTokens = max(1, (int)(strlen($rawContent) / 4)); } return [ 'success' => true, 'content' => $rawContent, 'json' => $parsedJson, 'model' => $response['json']['model'] ?? $model, 'provider' => 'openai', 'prompt_tokens' => $promptTokens, 'completion_tokens' => $completionTokens, 'total_tokens' => ($promptTokens + $completionTokens), ]; } /** * Hoofdfunctie: Voert een gestructureerde chat completion uit * Routeert automatisch naar Google Gemini Pro of OpenAI en dwingt ALTIJD * de Zero-Data Leakage privacy firewall af. * * @param array $messages [['role' => 'system'|'user'|'assistant', 'content' => string]] * @param array $options ['temperature' => float, 'json_mode' => bool, 'max_tokens' => int, 'user_id' => ?int] * @return array ['success' => bool, 'content' => ?string, 'json' => ?array, 'error' => ?string, 'provider' => ?string] */ function fociz_ai_chat(array $messages, array $options = []): array { // 1. ABSOLUTE ZERO-DATA LEAKAGE PRIVACY CONTROLE: // Filter ALLE Jira ticket keys, Atlassian URLs, Teams links en Mail adressen/headers fociz_ai_enforce_privacy_firewall($messages); $userId = $options['user_id'] ?? null; $aiSettings = get_ai_settings($userId); if (empty($aiSettings['ai_api_key'])) { return [ 'success' => false, 'error' => 'Geen AI API-sleutel geconfigureerd. Stel Google Gemini Pro of OpenAI in via Systeembeheer of je profiel.', ]; } $provider = strtolower($aiSettings['ai_provider'] ?? 'gemini'); $model = strtolower($aiSettings['ai_model'] ?? ''); // Routering: als provider gemini is of model gemini bevat, stuur naar Google Gemini if ($provider === 'gemini' || strpos($model, 'gemini') !== false) { $res = fociz_ai_chat_gemini($messages, $options, $aiSettings); } else { $res = fociz_ai_chat_openai($messages, $options, $aiSettings); } // Verbruiksstatistieken registreren bij geslaagde aanroep if (!empty($res['success'])) { $usedProvider = $res['provider'] ?? $provider; $usedModel = $res['model'] ?? $model; $pTokens = (int)($res['prompt_tokens'] ?? 0); $cTokens = (int)($res['completion_tokens'] ?? 0); record_ai_usage($usedProvider, $usedModel, $pTokens, $cTokens, $userId); } return $res; } /** * Werkt een brainstorm idee uit tot 3-5 concrete, behapbare items * voor een Kanban Bord, Sprint/Tijdlijn of Dagplanner. * * @param string $ideaContent * @param string $sessionTitle * @param string $theme * @param int|null $userId * @return array ['success' => bool, 'items' => array, 'error' => ?string] */ function fociz_ai_breakdown_idea(string $ideaContent, string $sessionTitle = '', string $theme = '', ?int $userId = null): array { $systemPrompt = "Je bent een senior Agile/Scrum product specialist en technisch ontwerper.\n" . "Je taak is om een brainstorm idee of concept om te zetten in 3 tot 5 concrete, behapbare en direct uitvoerbare taken voor een agile team.\n" . "Elk item moet een heldere taak zijn die binnen één dag of sprintblok voltooid kan worden.\n\n" . "Formateer de uitvoer als een JSON-object met exact deze structuur:\n" . "{\n" . " \"summary\": \"Korte samenvatting van de aanpak (1 zin)\",\n" . " \"items\": [\n" . " {\n" . " \"title\": \"Actiegerichte titel (bijv: API endpoint ontwerpen voor authenticatie)\",\n" . " \"description\": \"Korte toelichting en 2 concrete acceptatiecriteria\",\n" . " \"estimated_minutes\": 45,\n" . " \"priority\": \"P1\",\n" . " \"color\": \"indigo\",\n" . " \"category\": \"Backend\"\n" . " }\n" . " ]\n" . "}\n\n" . "Regels:\n" . "- Prioriteit moet 'P1' (hoog/focus), 'P2' (medium) of 'P3' (laag) zijn.\n" . "- Kleur moet gekozen worden uit: 'indigo', 'blue', 'emerald', 'amber', 'rose', 'purple'.\n" . "- Duur in minuten: 15, 30, 45, 60, 90 of 120.\n" . "- Categorie: 'Frontend', 'Backend', 'Design', 'Onderzoek', of 'Testing'.\n" . "- Retourneer UITSCHLUITEND geldige JSON."; $userPrompt = "Brainstorm Sessie: " . ($sessionTitle ?: 'Algemeen') . "\n" . "Thema: " . ($theme ?: 'Productontwikkeling') . "\n\n" . "Uit te werken idee:\n\"" . $ideaContent . "\"\n\n" . "Splits dit idee op in 3 tot 5 concrete items."; $aiRes = fociz_ai_chat([ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ], [ 'json_mode' => true, 'temperature' => 0.4, 'max_tokens' => 2000, 'user_id' => $userId, ]); if (!$aiRes['success']) { error_log("[fociz_ai_breakdown_idea] AI request failed: " . ($aiRes['error'] ?? 'Unknown error')); return [ 'success' => false, 'error' => $aiRes['error'] ?? 'Kon het idee niet ontleden in taken via AI.', ]; } $parsed = $aiRes['json'] ?? null; $items = []; $summary = ''; if (is_array($parsed)) { if (!empty($parsed['items']) && is_array($parsed['items'])) { $items = $parsed['items']; } elseif (!empty($parsed['tasks']) && is_array($parsed['tasks'])) { $items = $parsed['tasks']; } elseif (!empty($parsed['taken']) && is_array($parsed['taken'])) { $items = $parsed['taken']; } elseif (!empty($parsed['cards']) && is_array($parsed['cards'])) { $items = $parsed['cards']; } elseif (array_is_list($parsed)) { $items = $parsed; } $summary = (string)($parsed['summary'] ?? ''); } // Als JSON parsing toch leeg was, probeer regex fallback op ruwe content if (empty($items) && !empty($aiRes['content'])) { if (preg_match('/\[\s*\{.*\}\s*\]/s', $aiRes['content'], $mArr)) { $decodedArr = json_decode($mArr[0], true); if (is_array($decodedArr)) { $items = $decodedArr; } } } if (empty($items)) { error_log("[fociz_ai_breakdown_idea] Geen geldige items gevonden in response: " . substr((string)($aiRes['content'] ?? ''), 0, 500)); return [ 'success' => false, 'error' => 'De AI gaf een antwoord maar kon de taken niet structureren. Probeer het nogmaals.', ]; } return [ 'success' => true, 'summary' => $summary ?: 'Dit idee is opgesplitst in behapbare deeltaken.', 'items' => $items, 'model' => $aiRes['model'] ?? 'Gemini Pro', ]; } /** * Categoriseert sticky notes automatisch in: * - 'Ideeën' * - 'Knelpunten' * - 'Oplossingen' * - 'Quick Wins' */ function fociz_ai_categorize_cards(string $sessionTitle, string $theme, array $cards, ?int $userId = null): array { if (empty($cards)) { return ['success' => true, 'categorized' => []]; } $cardItems = []; foreach ($cards as $c) { $cardItems[] = [ 'id' => $c['id'], 'content' => $c['content'], 'category' => $c['category'] ?? 'Ideeën', ]; } $systemPrompt = "Je bent een deskundige agile facilitator en brainstorm co-pilot voor software- en productontwikkeling.\n" . "Je ontvangt een lijst met brainstorm kaartjes voor de sessie: '{$sessionTitle}' (Thema: '{$theme}').\n" . "Je taak is om elk kaartje in te delen in exact één van deze 4 categorieën:\n" . "1. 'Ideeën' (nieuwe concepten, functies, wensen)\n" . "2. 'Knelpunten' (problemen, blokkades, risico's, frustraties)\n" . "3. 'Oplossingen' (concrete antwoorden op knelpunten, architectuurkeuzes)\n" . "4. 'Quick Wins' (kleine, makkelijk uitvoerbare verbeteringen met hoge impact)\n\n" . "Retourneer uitsluitend een JSON-object met het veld 'categorized' dat een array is van objecten met 'id' en 'category'."; $userPrompt = "Hier zijn de kaartjes:\n" . json_encode($cardItems, JSON_UNESCAPED_UNICODE); $aiRes = fociz_ai_chat([ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ], ['json_mode' => true, 'user_id' => $userId]); if (!$aiRes['success'] || empty($aiRes['json']['categorized'])) { return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon kaartjes niet categoriseren']; } return ['success' => true, 'categorized' => $aiRes['json']['categorized']]; } /** * Genereert nieuwe relevante brainstorm ideeën voor een sessie */ function fociz_ai_generate_ideas(string $sessionTitle, string $theme, int $count = 4, ?int $userId = null): array { $systemPrompt = "Je bent een innovatieve productstrateeg en brainstorm co-pilot.\n" . "Genereer {$count} creatieve, scherpe en direct toepasbare ideeën voor een brainstormsessie getiteld: '{$sessionTitle}' (Thema: '{$theme}').\n" . "Houd de teksten beknopt en krachtig (1-2 zinnen per idee, alsof ze op een gele post-it staan).\n" . "Retourneer uitsluitend een JSON-object met het veld 'ideas' dat een array van strings is."; $aiRes = fociz_ai_chat([ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => "Genereer {$count} vernieuwende ideeën."], ], ['json_mode' => true, 'user_id' => $userId]); if (!$aiRes['success'] || empty($aiRes['json']['ideas'])) { return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen ideeën genereren']; } return ['success' => true, 'ideas' => $aiRes['json']['ideas']]; } /** * Zet brainstorm kaartjes om in een scherp Roadmap Item */ function fociz_ai_summarize_to_roadmap(string $content, string $sessionTitle = '', ?int $userId = null): array { $systemPrompt = "Je bent een senior product manager. Zet onderstaand brainstorm idee om in een helder, professioneel Roadmap Item.\n" . "Retourneer een JSON-object met:\n" . "- 'title': Een beknopte, actiegerichte titel (max 8 woorden)\n" . "- 'description': Een duidelijke beschrijving van wat er gebouwd moet worden inclusief gebruikerswaarde en 2-3 acceptatiecriteria\n" . "- 'priority': 'HIGH', 'MEDIUM' of 'LOW'\n" . "- 'type': 'FEATURE', 'IMPROVEMENT' of 'TECH_DEBT'\n" . "- 'quarter': 'Q3' of 'Q4'"; $aiRes = fociz_ai_chat([ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => "Sessie: {$sessionTitle}\nIdee:\n{$content}"], ], ['json_mode' => true, 'user_id' => $userId]); if (!$aiRes['success'] || empty($aiRes['json']['title'])) { return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon roadmap item niet formuleren']; } return ['success' => true, 'item' => $aiRes['json']]; } /** * Berekent een slimme dagindeling voor open taken rondom geanonimiseerde bezette tijdblokken. * ZERO-DATA LEAKAGE: Bevat NOOIT meetingtitels, inhoud of e-mailadressen! */ function fociz_ai_smart_schedule(array $tasks, array $busyBlocks, array $workdaySettings, ?int $userId = null): array { $startH = (int)explode(':', $workdaySettings['workday_start'] ?? '08:30')[0]; $endH = (int)explode(':', $workdaySettings['workday_end'] ?? '17:00')[0]; $lunchStart = $workdaySettings['lunch_start'] ?? '12:30'; $lunchEnd = $workdaySettings['lunch_end'] ?? '13:00'; // Anonimiseer taken: stuur alleen ID, titel (gefilterd door firewall), prioriteit en duur $sanitizedTasks = array_map(fn($t) => [ 'id' => (int)$t['id'], 'title' => (string)$t['title'], 'priority' => $t['priority'] ?? 'P2', 'duration_minutes' => (int)($t['duration_minutes'] ?? 30), ], $tasks); // Anonimiseer bezette blokken: GEEN titels of deelnemers, enkel tijdstippen! $sanitizedBusy = array_map(fn($b) => [ 'start' => $b['start'] ?? '09:00', 'end' => $b['end'] ?? '10:00', ], $busyBlocks); $systemPrompt = "Je bent een productiviteits- en time-blocking assistent.\n" . "Maak een optimale dagplanning door openstaande taken in te plannen tussen de werkdaguren ({$workdaySettings['workday_start']} tot {$workdaySettings['workday_end']}).\n" . "Regels:\n" . "1. Plan GEEN taken tijdens bezette blokken of tijdens de lunchpauze ({$lunchStart} - {$lunchEnd}).\n" . "2. Geef prioriteit aan P1 (brandend/focus) vroeg op de dag, daarna P2 en P3.\n" . "3. Zorg voor realistische tijdsblokken (15, 30, 45 of 60 minuten) met format 'HH:mm'.\n" . "Retourneer een JSON-object met het veld 'scheduled' als array van objecten: {'id': int, 'time_block_start': 'HH:mm', 'duration_minutes': int}."; $userPrompt = "Bezette tijdsintervallen (agenda):\n" . json_encode($sanitizedBusy, JSON_UNESCAPED_UNICODE) . "\n\n" . "In te plannen taken:\n" . json_encode($sanitizedTasks, JSON_UNESCAPED_UNICODE); $aiRes = fociz_ai_chat([ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ], ['json_mode' => true, 'user_id' => $userId]); if (!$aiRes['success'] || empty($aiRes['json']['scheduled'])) { return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen slimme indeling berekenen']; } return ['success' => true, 'scheduled' => $aiRes['json']['scheduled']]; } /** * Genereert een persoonlijke AI dagbriefing op basis van interne Fociz taken. * ZERO-DATA LEAKAGE: Absoluut GEEN Jira, Mail (Outlook) of Teams data! * * @param string $userName Naam van de gebruiker * @param string $date Datum (Y-m-d) * @param array $tasks Openstaande interne taken van vandaag * @param array $busyBlocks Geanonimiseerde bezette tijdsblokken (start/end) * @param array $unfinishedYest Gisteren niet voltooide taken * @param int|null $userId * @return array ['success' => bool, 'greeting' => string, 'focus_task' => string, 'tip' => string, 'warnings' => array] */ function fociz_ai_daily_briefing( string $userName, string $date, array $tasks, array $busyBlocks = [], array $unfinishedYest = [], ?int $userId = null ): array { $dutchDays = ['', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag', 'zondag']; $dayName = $dutchDays[(int)date('N', strtotime($date))] ?? 'vandaag'; $dateLabel = $dayName . ' ' . date('j', strtotime($date)) . ' ' . date('F', strtotime($date)); // Alleen pure Fociz taken samenvatten $taskSummary = array_map(fn($t) => [ 'title' => $t['title'], 'priority' => $t['priority'] ?? 'P2', 'time' => $t['time_block_start'] ?? null, 'duration' => (int)($t['duration_minutes'] ?? 30), ], array_slice($tasks, 0, 10)); $yesterdaySummary = array_map(fn($t) => $t['title'], array_slice($unfinishedYest, 0, 5)); $systemPrompt = "Je bent de persoonlijke AI Co-Pilot van {$userName} in Fociz. " . "Schrijf een korte, energieke en persoonlijke dagbriefing in het Nederlands voor {$dateLabel}. " . "Wees direct, concreet en vriendelijk. Gebruik geen markdown headers, geen bullet symbolen zoals *, -, •. " . "Geef 1-2 zinnen dagopening + 1 zin over de grootste prioriteit + 1 praktische focustip. " . "Maximaal 60 woorden totaal. " . "Retourneer een JSON-object met:\n" . "- 'greeting': openingszin (max 20 woorden)\n" . "- 'focus_task': de meest urgente taak vandaag (max 15 woorden)\n" . "- 'tip': één concrete focustip voor vandaag (max 20 woorden)\n" . "- 'warnings': array van strings, max 2 items (bijv. gisteren niet afgerond of overvolle dag)"; $userPrompt = json_encode([ 'date' => $dateLabel, 'tasks_today' => $taskSummary, 'busy_blocks_count' => count($busyBlocks), 'unfinished_yesterday' => $yesterdaySummary, 'total_tasks' => count($tasks), ], JSON_UNESCAPED_UNICODE); $aiRes = fociz_ai_chat([ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ], ['json_mode' => true, 'temperature' => 0.6, 'max_tokens' => 300, 'user_id' => $userId]); if (!$aiRes['success'] || empty($aiRes['json'])) { return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen dagbriefing genereren']; } return [ 'success' => true, 'greeting' => $aiRes['json']['greeting'] ?? "Goedemorgen {$userName}, een nieuwe dag!", 'focus_task' => $aiRes['json']['focus_task'] ?? '', 'tip' => $aiRes['json']['tip'] ?? '', 'warnings' => $aiRes['json']['warnings'] ?? [], 'model' => $aiRes['model'] ?? 'AI', ]; }