Fix consistentie benamingen.
This commit is contained in:
+331
-63
@@ -8,21 +8,185 @@ require_once __DIR__ . '/http.php';
|
||||
require_once __DIR__ . '/settings.php';
|
||||
|
||||
/**
|
||||
* Voert een gestructureerde chat completion uit met de geconfigureerde AI provider
|
||||
* STRICT ZERO-DATA LEAKAGE PRIVACY FIREWALL
|
||||
* Garandeert dat NOOIT data uit Jira, Mail (Outlook) of Microsoft Teams
|
||||
* doorgestuurd kan worden naar een externe AI provider (Gemini / OpenAI).
|
||||
*
|
||||
* @param array $messages [['role' => 'system'|'user'|'assistant', 'content' => string]]
|
||||
* @param array $options ['temperature' => float, 'json_mode' => bool, 'max_tokens' => int]
|
||||
* @return array ['success' => bool, 'content' => ?string, 'json' => ?array, 'error' => ?string]
|
||||
* @param array $messages Referentie naar de messages array die verzonden wordt
|
||||
* @return array Aantal en type geactiveerde firewall filters
|
||||
*/
|
||||
function fociz_ai_chat(array $messages, array $options = []): array {
|
||||
$aiSettings = get_ai_settings();
|
||||
if (empty($aiSettings['ai_api_key'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Geen AI API-sleutel geconfigureerd. Stel deze in via Systeembeheer (admin.php).',
|
||||
function fociz_ai_enforce_privacy_firewall(array &$messages): array {
|
||||
$detections = [
|
||||
'jira_tickets' => 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 = $aiSettings['ai_api_key'];
|
||||
$model = !empty($aiSettings['ai_model']) ? $aiSettings['ai_model'] : 'gemini-1.5-pro';
|
||||
$temperature = $options['temperature'] ?? 0.3;
|
||||
$maxTokens = $options['max_tokens'] ?? 2048;
|
||||
$jsonMode = !empty($options['json_mode']);
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
$endpoint = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . urlencode($apiKey);
|
||||
|
||||
$response = fociz_http_request($endpoint, [
|
||||
'method' => 'POST',
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'body' => $payload,
|
||||
'timeout' => 45,
|
||||
]);
|
||||
|
||||
if ($response['status'] !== 200 || empty($response['json']['candidates'][0]['content']['parts'][0]['text'])) {
|
||||
$errMsg = $response['json']['error']['message'] ?? ($response['error'] ?? "Gemini API aanroep mislukt (HTTP {$response['status']})");
|
||||
error_log("fociz_ai_chat_gemini fout: {$errMsg}");
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $errMsg,
|
||||
];
|
||||
}
|
||||
|
||||
$rawContent = trim($response['json']['candidates'][0]['content']['parts'][0]['text']);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'content' => $cleanContent,
|
||||
'json' => $parsedJson,
|
||||
'model' => $model,
|
||||
'provider' => 'gemini',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = $aiSettings['ai_api_key'];
|
||||
$model = !empty($aiSettings['ai_model']) ? $aiSettings['ai_model'] : 'gpt-4o-mini';
|
||||
$temperature = $options['temperature'] ?? 0.3;
|
||||
@@ -51,8 +215,8 @@ function fociz_ai_chat(array $messages, array $options = []): array {
|
||||
]);
|
||||
|
||||
if ($response['status'] !== 200 || empty($response['json']['choices'][0]['message']['content'])) {
|
||||
$errMsg = $response['json']['error']['message'] ?? ($response['error'] ?? "AI aanroep mislukt (HTTP {$response['status']})");
|
||||
error_log("fociz_ai_chat fout: {$errMsg}");
|
||||
$errMsg = $response['json']['error']['message'] ?? ($response['error'] ?? "OpenAI aanroep mislukt (HTTP {$response['status']})");
|
||||
error_log("fociz_ai_chat_openai fout: {$errMsg}");
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $errMsg,
|
||||
@@ -66,11 +230,112 @@ function fociz_ai_chat(array $messages, array $options = []): array {
|
||||
$parsedJson = json_decode($rawContent, true);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'content' => $rawContent,
|
||||
'json' => $parsedJson,
|
||||
'model' => $response['json']['model'] ?? $model,
|
||||
'provider' => 'openai',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return fociz_ai_chat_gemini($messages, $options, $aiSettings);
|
||||
}
|
||||
|
||||
return fociz_ai_chat_openai($messages, $options, $aiSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] || empty($aiRes['json']['items'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $aiRes['error'] ?? 'Kon het idee niet ontleden in taken via AI.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'content' => $rawContent,
|
||||
'json' => $parsedJson,
|
||||
'model' => $response['json']['model'] ?? $model,
|
||||
'summary' => $aiRes['json']['summary'] ?? '',
|
||||
'items' => $aiRes['json']['items'],
|
||||
'model' => $aiRes['model'] ?? 'Gemini Pro',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -81,7 +346,7 @@ function fociz_ai_chat(array $messages, array $options = []): array {
|
||||
* - 'Oplossingen'
|
||||
* - 'Quick Wins'
|
||||
*/
|
||||
function fociz_ai_categorize_cards(string $sessionTitle, string $theme, array $cards): array {
|
||||
function fociz_ai_categorize_cards(string $sessionTitle, string $theme, array $cards, ?int $userId = null): array {
|
||||
if (empty($cards)) {
|
||||
return ['success' => true, 'categorized' => []];
|
||||
}
|
||||
@@ -109,7 +374,7 @@ function fociz_ai_categorize_cards(string $sessionTitle, string $theme, array $c
|
||||
$aiRes = fociz_ai_chat([
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $userPrompt],
|
||||
], ['json_mode' => true]);
|
||||
], ['json_mode' => true, 'user_id' => $userId]);
|
||||
|
||||
if (!$aiRes['success'] || empty($aiRes['json']['categorized'])) {
|
||||
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon kaartjes niet categoriseren'];
|
||||
@@ -121,7 +386,7 @@ function fociz_ai_categorize_cards(string $sessionTitle, string $theme, array $c
|
||||
/**
|
||||
* Genereert nieuwe relevante brainstorm ideeën voor een sessie
|
||||
*/
|
||||
function fociz_ai_generate_ideas(string $sessionTitle, string $theme, int $count = 4): array {
|
||||
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"
|
||||
@@ -130,7 +395,7 @@ function fociz_ai_generate_ideas(string $sessionTitle, string $theme, int $count
|
||||
$aiRes = fociz_ai_chat([
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => "Genereer {$count} vernieuwende ideeën."],
|
||||
], ['json_mode' => true]);
|
||||
], ['json_mode' => true, 'user_id' => $userId]);
|
||||
|
||||
if (!$aiRes['success'] || empty($aiRes['json']['ideas'])) {
|
||||
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen ideeën genereren'];
|
||||
@@ -142,7 +407,7 @@ function fociz_ai_generate_ideas(string $sessionTitle, string $theme, int $count
|
||||
/**
|
||||
* Zet brainstorm kaartjes om in een scherp Roadmap Item
|
||||
*/
|
||||
function fociz_ai_summarize_to_roadmap(string $content, string $sessionTitle = ''): array {
|
||||
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"
|
||||
@@ -154,7 +419,7 @@ function fociz_ai_summarize_to_roadmap(string $content, string $sessionTitle = '
|
||||
$aiRes = fociz_ai_chat([
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => "Sessie: {$sessionTitle}\nIdee:\n{$content}"],
|
||||
], ['json_mode' => true]);
|
||||
], ['json_mode' => true, 'user_id' => $userId]);
|
||||
|
||||
if (!$aiRes['success'] || empty($aiRes['json']['title'])) {
|
||||
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon roadmap item niet formuleren'];
|
||||
@@ -164,29 +429,44 @@ function fociz_ai_summarize_to_roadmap(string $content, string $sessionTitle = '
|
||||
}
|
||||
|
||||
/**
|
||||
* Berekent een slimme dagindeling voor open taken rondom bestaande afspraken
|
||||
* 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 $events, array $workdaySettings): array {
|
||||
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 bestaande afspraken of tijdens de lunchpauze ({$lunchStart} - {$lunchEnd}).\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 = "Bestaande afspraken:\n" . json_encode($events, JSON_UNESCAPED_UNICODE) . "\n\n"
|
||||
. "In te plannen taken:\n" . json_encode($tasks, JSON_UNESCAPED_UNICODE);
|
||||
$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]);
|
||||
], ['json_mode' => true, 'user_id' => $userId]);
|
||||
|
||||
if (!$aiRes['success'] || empty($aiRes['json']['scheduled'])) {
|
||||
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen slimme indeling berekenen'];
|
||||
@@ -196,28 +476,30 @@ function fociz_ai_smart_schedule(array $tasks, array $events, array $workdaySett
|
||||
}
|
||||
|
||||
/**
|
||||
* Genereert een persoonlijke AI dagbriefing op basis van taken, afspraken en Jira tickets
|
||||
* 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 taken van vandaag
|
||||
* @param array $meetings Outlook meetings van vandaag
|
||||
* @param array $jiraIssues Jira tickets (P1/blokkades)
|
||||
* @param array $tasks Openstaande interne taken van vandaag
|
||||
* @param array $busyBlocks Geanonimiseerde bezette tijdsblokken (start/end)
|
||||
* @param array $unfinishedYest Gisteren niet voltooide taken
|
||||
* @return array ['success' => bool, 'briefing' => string, 'focus_tip' => string, 'warnings' => array]
|
||||
* @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 $meetings,
|
||||
array $jiraIssues = [],
|
||||
array $unfinishedYest = []
|
||||
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',
|
||||
@@ -225,56 +507,42 @@ function fociz_ai_daily_briefing(
|
||||
'duration' => (int)($t['duration_minutes'] ?? 30),
|
||||
], array_slice($tasks, 0, 10));
|
||||
|
||||
$meetingSummary = array_map(fn($e) => [
|
||||
'subject' => $e['subject'] ?? 'Afspraak',
|
||||
'start' => $e['start'] ?? null,
|
||||
'end' => $e['end'] ?? null,
|
||||
], array_slice($meetings, 0, 8));
|
||||
|
||||
$jiraSummary = array_map(fn($j) => [
|
||||
'key' => $j['key'] ?? '',
|
||||
'title' => $j['summary'] ?? '',
|
||||
'priority' => $j['priority'] ?? 'Medium',
|
||||
'status' => $j['status'] ?? 'Open',
|
||||
], array_slice(array_filter($jiraIssues, fn($j) => in_array($j['priority'] ?? '', ['Highest', 'High'])), 0, 5));
|
||||
|
||||
$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 praktisch focustip. "
|
||||
. "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 of meeting vandaag (max 15 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 — alleen als er iets opvalt (gisteren niet afgerond, urgent Jira, overvolle dag)";
|
||||
. "- 'warnings': array van strings, max 2 items (bijv. gisteren niet afgerond of overvolle dag)";
|
||||
|
||||
$userPrompt = json_encode([
|
||||
'date' => $dateLabel,
|
||||
'tasks_today' => $taskSummary,
|
||||
'meetings_today' => $meetingSummary,
|
||||
'urgent_jira' => $jiraSummary,
|
||||
'date' => $dateLabel,
|
||||
'tasks_today' => $taskSummary,
|
||||
'busy_blocks_count' => count($busyBlocks),
|
||||
'unfinished_yesterday' => $yesterdaySummary,
|
||||
'total_tasks' => count($tasks),
|
||||
'total_meetings' => count($meetings),
|
||||
'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]);
|
||||
], ['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'] ?? [],
|
||||
'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',
|
||||
];
|
||||
}
|
||||
|
||||
+47
-4
@@ -343,6 +343,11 @@ function ensure_remember_token_columns(PDO $db): void {
|
||||
// Schoon uitsluitend verlopen tokens van gisteren op, wis NOOIT actieve tokens van vandaag
|
||||
$db->exec("UPDATE `users` SET `remember_token` = NULL, `remember_token_expires_at` = NULL WHERE `remember_token_expires_at` IS NOT NULL AND `remember_token_expires_at` < NOW()");
|
||||
}
|
||||
|
||||
$resActivity = $db->query("SHOW COLUMNS FROM `users` LIKE 'last_activity_at'");
|
||||
if (!$resActivity || $resActivity->rowCount() === 0) {
|
||||
$db->exec("ALTER TABLE `users` ADD COLUMN `last_activity_at` DATETIME NULL AFTER `remember_token_expires_at`");
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Stil negeren indien al aanwezig of tijdens migraties
|
||||
}
|
||||
@@ -532,7 +537,7 @@ function issue_day_session_token(int $userId): string {
|
||||
|
||||
$db = get_db_connection();
|
||||
ensure_remember_token_columns($db);
|
||||
$stmt = $db->prepare("UPDATE users SET remember_token = ?, remember_token_expires_at = ? WHERE id = ?");
|
||||
$stmt = $db->prepare("UPDATE users SET remember_token = ?, remember_token_expires_at = ?, last_activity_at = NOW() WHERE id = ?");
|
||||
$stmt->execute([$token, $expiresAt, $userId]);
|
||||
|
||||
$isSecure = is_https_request();
|
||||
@@ -591,6 +596,14 @@ function get_logged_in_user(): ?array {
|
||||
$today = date('Y-m-d');
|
||||
$now = time();
|
||||
|
||||
// Bepaal of dit een geautomatiseerde achtergrond poll is (zoals notifications, live sync of outlook polling)
|
||||
$isBackgroundPoll = !empty($_SERVER['HTTP_X_BACKGROUND_POLL']) ||
|
||||
(isset($_SERVER['REQUEST_URI']) && (
|
||||
strpos($_SERVER['REQUEST_URI'], '/api/live_sync.php') !== false ||
|
||||
(strpos($_SERVER['REQUEST_URI'], '/api/notifications.php') !== false && ($_GET['action'] ?? '') === 'get_events') ||
|
||||
(strpos($_SERVER['REQUEST_URI'], '/api/planner.php') !== false && ($_GET['action'] ?? '') === 'check_outlook_sync')
|
||||
));
|
||||
|
||||
// 1. Controleer of de huidige sessie van een eerdere dag is (dagwissel om middernacht)
|
||||
if (!empty($_SESSION['auth_date']) && $_SESSION['auth_date'] !== $today) {
|
||||
$_SESSION = ['session_was_expired' => true];
|
||||
@@ -668,7 +681,19 @@ function get_logged_in_user(): ?array {
|
||||
$_SESSION['auth_date'] = $today;
|
||||
$_SESSION['auth_fingerprint'] = $clientFingerprint;
|
||||
$_SESSION['session_expires_at'] = strtotime('today 23:59:59');
|
||||
$_SESSION['last_activity'] = $now;
|
||||
|
||||
// Achtergrondpolls verversen last_activity NIET; alleen daadwerkelijke gebruikersinteractie houdt de sessie actief
|
||||
if (!$isBackgroundPoll) {
|
||||
$_SESSION['last_activity'] = $now;
|
||||
if (empty($_SESSION['last_db_activity_update']) || ($now - (int)$_SESSION['last_db_activity_update']) > 60) {
|
||||
$_SESSION['last_db_activity_update'] = $now;
|
||||
try {
|
||||
$stmtAct = $db->prepare("UPDATE users SET last_activity_at = NOW() WHERE id = ?");
|
||||
$stmtAct->execute([$_SESSION['user_id']]);
|
||||
} catch (Throwable $e) {}
|
||||
}
|
||||
}
|
||||
|
||||
$user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']);
|
||||
$_SESSION['user_avatar_url'] = $user['avatar_url'];
|
||||
return $user;
|
||||
@@ -679,13 +704,25 @@ function get_logged_in_user(): ?array {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. Fallback dagsessie-token: herstelt sessie na browserherstart (uitsluitend vandaag!)
|
||||
// 3. Fallback dagsessie-token: herstelt sessie na browserherstart (uitsluitend vandaag én mits niet inactief!)
|
||||
$dayToken = $_COOKIE['fociz_day_session'] ?? ($_COOKIE['fociz_remember'] ?? null);
|
||||
if (!empty($dayToken) && is_string($dayToken)) {
|
||||
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, fociz_widget_layout, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?");
|
||||
$stmt = $db->prepare("SELECT id, name, email, role, two_factor_enabled, avatar_url, theme, notification_settings, default_jira_filter_code, fociz_widget_layout, last_activity_at, last_login_at, created_at FROM users WHERE remember_token = ? AND remember_token_expires_at >= NOW() AND DATE(remember_token_expires_at) = ?");
|
||||
$stmt->execute([$dayToken, $today]);
|
||||
$user = $stmt->fetch();
|
||||
if ($user) {
|
||||
$maxIdleSeconds = function_exists('get_session_idle_timeout') ? get_session_idle_timeout() : 1800;
|
||||
// Controleer inactiviteit: als gebruiker te lang inactief is geweest, herstel weigeren en opnieuw inloggen
|
||||
$lastActTime = !empty($user['last_activity_at']) ? strtotime($user['last_activity_at']) : (!empty($user['last_login_at']) ? strtotime($user['last_login_at']) : 0);
|
||||
if ($lastActTime > 0 && ($now - $lastActTime) > $maxIdleSeconds) {
|
||||
fociz_log_audit((int)$user['id'], 'SESSION_IDLE_TIMEOUT_COOKIE', 'users', (int)$user['id'], ['idle_seconds' => $now - $lastActTime, 'max_idle' => $maxIdleSeconds], 'WARNING');
|
||||
$stmtClear = $db->prepare("UPDATE users SET remember_token = NULL, remember_token_expires_at = NULL WHERE id = ?");
|
||||
$stmtClear->execute([$user['id']]);
|
||||
$_SESSION = ['session_was_expired' => true, 'idle_timeout' => true];
|
||||
clear_day_session_cookie();
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentIp = fociz_get_client_ip();
|
||||
// Als het token op een ander, onbekend IP wordt aangeboden, weiger de automatische herstel
|
||||
if (!fociz_is_ip_known_for_user((int)$user['id'], $currentIp)) {
|
||||
@@ -705,6 +742,12 @@ function get_logged_in_user(): ?array {
|
||||
$_SESSION['last_activity'] = $now;
|
||||
$_SESSION['login_time'] = $now;
|
||||
$_SESSION['last_rotated'] = $now;
|
||||
$_SESSION['last_db_activity_update'] = $now;
|
||||
try {
|
||||
$stmtAct = $db->prepare("UPDATE users SET last_activity_at = NOW() WHERE id = ?");
|
||||
$stmtAct->execute([$user['id']]);
|
||||
} catch (Throwable $e) {}
|
||||
|
||||
$user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']);
|
||||
$_SESSION['user_avatar_url'] = $user['avatar_url'];
|
||||
return $user;
|
||||
|
||||
+65
-16
@@ -106,26 +106,75 @@ function get_workday_settings(): array {
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt AI configuratie-instellingen op (Release v1.5)
|
||||
* Haalt AI configuratie-instellingen op (Release v2.0 - Google Gemini Pro & OpenAI)
|
||||
* Ondersteunt organisatiebrede en optioneel persoonlijke sleutels.
|
||||
*/
|
||||
function get_ai_settings(): array {
|
||||
$envKey = fociz_env('OPENAI_API_KEY', '');
|
||||
$savedKeyRaw = get_system_setting('ai_api_key', '');
|
||||
$savedKey = '';
|
||||
if (!empty($savedKeyRaw)) {
|
||||
if (!function_exists('fociz_decrypt')) {
|
||||
require_once __DIR__ . '/auth.php';
|
||||
}
|
||||
$savedKey = fociz_decrypt($savedKeyRaw);
|
||||
function get_ai_settings(?int $userId = null): array {
|
||||
if (!function_exists('fociz_decrypt')) {
|
||||
require_once __DIR__ . '/auth.php';
|
||||
}
|
||||
$apiKey = !empty($savedKey) ? $savedKey : $envKey;
|
||||
|
||||
$apiKey = '';
|
||||
$isPersonal = false;
|
||||
|
||||
// 1. Controleer persoonlijke sleutel van de ingelogde gebruiker
|
||||
if ($userId !== null && $userId > 0) {
|
||||
try {
|
||||
$db = get_db_connection();
|
||||
$stmt = $db->prepare("SELECT access_token, settings, is_enabled FROM integrations WHERE user_id = ? AND provider IN ('GEMINI', 'AI') AND is_enabled = 1 ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute([$userId]);
|
||||
$row = $stmt->fetch();
|
||||
if ($row) {
|
||||
// Sleutel kan in access_token staan of in settings['api_key']
|
||||
if (!empty($row['access_token'])) {
|
||||
$apiKey = fociz_decrypt($row['access_token']);
|
||||
$isPersonal = true;
|
||||
} elseif (!empty($row['settings'])) {
|
||||
$userSettings = json_decode($row['settings'], true) ?: [];
|
||||
if (!empty($userSettings['api_key'])) {
|
||||
$apiKey = fociz_decrypt($userSettings['api_key']);
|
||||
$isPersonal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($row['settings'])) {
|
||||
$userSettings = json_decode($row['settings'], true) ?: [];
|
||||
if (!empty($userSettings['provider'])) {
|
||||
$personalProvider = $userSettings['provider'];
|
||||
}
|
||||
if (!empty($userSettings['model'])) {
|
||||
$personalModel = $userSettings['model'];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("Fout bij ophalen persoonlijke AI sleutel: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback naar organisatiebrede instellingen
|
||||
if (empty($apiKey)) {
|
||||
$savedKeyRaw = get_system_setting('ai_api_key', '');
|
||||
if (!empty($savedKeyRaw)) {
|
||||
$apiKey = fociz_decrypt($savedKeyRaw);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback naar .env variabelen (GEMINI_API_KEY of OPENAI_API_KEY)
|
||||
if (empty($apiKey)) {
|
||||
$apiKey = fociz_env('GEMINI_API_KEY', '') ?: fociz_env('OPENAI_API_KEY', '');
|
||||
}
|
||||
|
||||
$provider = !empty($personalProvider) ? $personalProvider : get_system_setting('ai_provider', 'gemini');
|
||||
$model = !empty($personalModel) ? $personalModel : get_system_setting('ai_model', 'gemini-1.5-pro');
|
||||
|
||||
return [
|
||||
'ai_enabled' => (bool)get_system_setting('ai_enabled', !empty($apiKey)),
|
||||
'ai_provider' => get_system_setting('ai_provider', 'openai'),
|
||||
'ai_api_key' => $apiKey,
|
||||
'ai_model' => get_system_setting('ai_model', 'gpt-4o-mini'),
|
||||
'has_key' => !empty($apiKey),
|
||||
'ai_enabled' => (bool)get_system_setting('ai_enabled', !empty($apiKey)),
|
||||
'ai_provider' => $provider,
|
||||
'ai_api_key' => $apiKey,
|
||||
'ai_model' => $model,
|
||||
'has_key' => !empty($apiKey),
|
||||
'is_personal_key'=> $isPersonal,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user