From 74d53c800e7e47498ad311afb6e23b7de8951897 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 11 Apr 2026 19:22:11 +0000 Subject: [PATCH] =?UTF-8?q?Dodanie=20AI=20do=20raport=C3=B3w?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/bootstrap.php | 1 + app/diagnostic_functions.php | 421 +++++++++++++++++++++++++++++++---- 2 files changed, 374 insertions(+), 48 deletions(-) diff --git a/app/bootstrap.php b/app/bootstrap.php index 8978c12..075738b 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -9,6 +9,7 @@ date_default_timezone_set('UTC'); require_once __DIR__ . '/../db/config.php'; require_once __DIR__ . '/../mail/MailService.php'; +require_once __DIR__ . '/../ai/LocalAIApi.php'; require_once __DIR__ . '/diagnostic_data.php'; require_once __DIR__ . '/diagnostic_functions.php'; diff --git a/app/diagnostic_functions.php b/app/diagnostic_functions.php index de6b7a9..05da766 100644 --- a/app/diagnostic_functions.php +++ b/app/diagnostic_functions.php @@ -13,6 +13,10 @@ function ensure_diagnostic_schema(): void current_step INT UNSIGNED NOT NULL DEFAULT 1, answers_json LONGTEXT NULL, result_json LONGTEXT NULL, + report_subject VARCHAR(255) NULL, + report_html LONGTEXT NULL, + report_text LONGTEXT NULL, + report_generated_at DATETIME NULL, email_report_status VARCHAR(20) NOT NULL DEFAULT 'pending', email_report_error TEXT NULL, contact_phone VARCHAR(40) NULL, @@ -25,6 +29,22 @@ function ensure_diagnostic_schema(): void ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); + if (!diagnostic_schema_column_exists('diagnostic_attempts', 'report_subject')) { + db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN report_subject VARCHAR(255) NULL AFTER result_json"); + } + + if (!diagnostic_schema_column_exists('diagnostic_attempts', 'report_html')) { + db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN report_html LONGTEXT NULL AFTER report_subject"); + } + + if (!diagnostic_schema_column_exists('diagnostic_attempts', 'report_text')) { + db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN report_text LONGTEXT NULL AFTER report_html"); + } + + if (!diagnostic_schema_column_exists('diagnostic_attempts', 'report_generated_at')) { + db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN report_generated_at DATETIME NULL AFTER report_text"); + } + if (!diagnostic_schema_column_exists('diagnostic_attempts', 'contact_phone')) { db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN contact_phone VARCHAR(40) NULL AFTER email_report_error"); } @@ -317,7 +337,15 @@ function diagnostic_finalize_attempt(int $attemptId): ?array $result = diagnostic_calculate_result($attempt['answers']); $stmt = db()->prepare( 'UPDATE diagnostic_attempts - SET status = :status, current_step = :current_step, result_json = :result_json, completed_at = NOW(), updated_at = NOW() + SET status = :status, + current_step = :current_step, + result_json = :result_json, + report_subject = NULL, + report_html = NULL, + report_text = NULL, + report_generated_at = NULL, + completed_at = NOW(), + updated_at = NOW() WHERE id = :id' ); $stmt->execute([ @@ -414,90 +442,387 @@ function diagnostic_email_data_table(array $rows): string } -function diagnostic_build_report_html(array $attempt): string +function diagnostic_email_list(array $items, string $accentColor = '#111827'): string +{ + $html = ''; +} + +function diagnostic_report_limit(string $value, int $maxLength): string +{ + $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', '', $value) ?? $value; + $value = str_replace(["\r\n", "\r"], "\n", $value); + $value = preg_replace('/[ \t]+/u', ' ', $value) ?? $value; + $value = preg_replace("/\n{3,}/u", "\n\n", $value) ?? $value; + $value = trim($value); + + if ($maxLength > 0) { + if (function_exists('mb_substr')) { + $value = mb_substr($value, 0, $maxLength, 'UTF-8'); + } else { + $value = substr($value, 0, $maxLength); + } + $value = trim($value); + } + + return $value; +} + +function diagnostic_report_list(array $items, int $maxItems = 3, int $maxLength = 220): array +{ + $normalized = []; + foreach ($items as $item) { + if (!is_scalar($item)) { + continue; + } + $line = diagnostic_report_limit((string)$item, $maxLength); + if ($line !== '') { + $normalized[] = $line; + } + if (count($normalized) >= $maxItems) { + break; + } + } + return $normalized; +} + +function diagnostic_question_answer_rows(array $attempt): array +{ + $rows = []; + $questions = diagnostic_flat_questions(); + $answers = $attempt['answers'] ?? []; + foreach ($questions as $question) { + $value = $answers[$question['id']] ?? null; + if ($value === null || $value === '') { + continue; + } + $rows[] = [ + 'section' => $question['section_name'], + 'question' => $question['text'], + 'answer_value' => (int)$value, + 'answer_label' => diagnostic_answer_label((int)$value), + 'reverse' => !empty($question['reverse']), + ]; + } + return $rows; +} + +function diagnostic_default_report_copy(array $attempt): array +{ + $result = $attempt['result'] ?? []; + $strengths = []; + foreach (($result['strongest_areas'] ?? []) as $area) { + $strengths[] = sprintf( + '%s (%d%%) — to obszar, który już daje Ci relatywnie stabilny fundament operacyjny.', + (string)($area['section_name'] ?? 'Mocny obszar'), + (int)($area['percentage'] ?? 0) + ); + } + + $priorities = []; + foreach (($result['weakest_areas'] ?? []) as $area) { + $priorities[] = sprintf( + '%s (%d%%) — tutaj najszybciej pojawiają się tarcia, opóźnienia lub zależność od ręcznego nadzoru.', + (string)($area['section_name'] ?? 'Priorytet'), + (int)($area['percentage'] ?? 0) + ); + } + + $nextSteps = []; + foreach (($result['recommendations'] ?? []) as $item) { + $nextSteps[] = (string)$item; + } + + $segmentLabel = (string)($result['segment_label'] ?? 'Diagnoza zakończona'); + $segmentSummary = diagnostic_report_limit((string)($result['segment_summary'] ?? ''), 420); + $priorityNames = array_map( + static fn(array $area): string => (string)($area['section_name'] ?? ''), + (array)($result['weakest_areas'] ?? []) + ); + $priorityNames = array_values(array_filter($priorityNames, static fn(string $name): bool => trim($name) !== '')); + $whatItMeans = 'Największy potencjał poprawy widać obecnie w obszarach: ' . ($priorityNames ? implode(', ', $priorityNames) : 'organizacji codziennej pracy') . '.'; + + return [ + 'subject' => 'Twój raport diagnozy dojrzałości procesowej', + 'preheader' => 'Gotowe: wynik diagnozy i rekomendowane kolejne kroki dla Twojej firmy.', + 'intro' => 'Dziękujemy za wypełnienie diagnozy. Poniżej znajdziesz krótkie podsumowanie wyniku oraz rekomendowane następne kroki.', + 'executive_summary' => $segmentSummary !== '' ? $segmentSummary : 'Masz już gotowy wynik diagnozy oraz wskazanie obszarów, które warto uporządkować w pierwszej kolejności.', + 'what_it_means' => $whatItMeans, + 'strengths' => diagnostic_report_list($strengths), + 'priorities' => diagnostic_report_list($priorities), + 'next_steps' => diagnostic_report_list($nextSteps), + 'closing' => 'Wróć do podsumowania diagnozy, aby omówić wynik z zespołem lub zaplanować kolejny krok wdrożeniowy.', + ]; +} + +function diagnostic_ai_report_prompt_payload(array $attempt): array +{ + $result = $attempt['result'] ?? []; + + return [ + 'quiz' => [ + 'title' => diagnostic_quiz_definition()['quiz']['title'] ?? 'Diagnoza dojrzałości procesowej', + 'language' => 'pl', + ], + 'recipient' => [ + 'email' => (string)($attempt['email'] ?? ''), + ], + 'result' => [ + 'score_percent' => (int)($result['percentage_score'] ?? 0), + 'segment_label' => (string)($result['segment_label'] ?? ''), + 'segment_summary' => (string)($result['segment_summary'] ?? ''), + 'recommendations' => array_values((array)($result['recommendations'] ?? [])), + 'strongest_areas' => array_values((array)($result['strongest_areas'] ?? [])), + 'weakest_areas' => array_values((array)($result['weakest_areas'] ?? [])), + 'section_scores' => array_values((array)($result['section_scores'] ?? [])), + ], + 'answers' => diagnostic_question_answer_rows($attempt), + ]; +} + +function diagnostic_normalize_report_copy(array $candidate, array $defaults): array +{ + $copy = $defaults; + foreach (['subject' => 140, 'preheader' => 180, 'intro' => 420, 'executive_summary' => 650, 'what_it_means' => 520, 'closing' => 320] as $field => $maxLength) { + if (!empty($candidate[$field]) && is_scalar($candidate[$field])) { + $normalized = diagnostic_report_limit((string)$candidate[$field], $maxLength); + if ($normalized !== '') { + $copy[$field] = $normalized; + } + } + } + + foreach (['strengths', 'priorities', 'next_steps'] as $field) { + if (!empty($candidate[$field]) && is_array($candidate[$field])) { + $normalizedList = diagnostic_report_list($candidate[$field]); + if ($normalizedList !== []) { + $copy[$field] = $normalizedList; + } + } + } + + return $copy; +} + +function diagnostic_generate_ai_report_copy(array $attempt): array +{ + $defaults = diagnostic_default_report_copy($attempt); + $payload = diagnostic_ai_report_prompt_payload($attempt); + + try { + $response = LocalAIApi::createResponse( + [ + 'input' => [ + [ + 'role' => 'system', + 'content' => 'Jesteś konsultantem operacyjnym B2B. Tworzysz krótkie, konkretne i profesjonalne podsumowania e-mail po polsku na podstawie danych z quizu. Odpowiadaj wyłącznie poprawnym JSON-em UTF-8 bez markdownu i bez HTML. Zachowaj rzeczowy ton. Nie wymyślaj faktów wykraczających poza dane. Zwróć dokładnie pola: subject, preheader, intro, executive_summary, what_it_means, strengths, priorities, next_steps, closing. strengths/priorities/next_steps mają być tablicami 2-3 krótkich punktów.' + ], + [ + 'role' => 'user', + 'content' => "Przygotuj treść raportu e-mail dla klienta na podstawie tych danych quizu. Dane wejściowe JSON:\n" . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) + ], + ], + ], + [ + 'poll_interval' => 3, + 'poll_timeout' => 120, + ] + ); + + if (!empty($response['success'])) { + $decoded = LocalAIApi::decodeJsonFromResponse($response); + if (is_array($decoded)) { + return [ + 'copy' => diagnostic_normalize_report_copy($decoded, $defaults), + 'source' => 'ai', + ]; + } + + error_log('Diagnostic AI report generation returned unparsable payload.'); + } else { + error_log('Diagnostic AI report generation failed: ' . (string)($response['error'] ?? $response['message'] ?? 'unknown_error')); + } + } catch (Throwable $exception) { + error_log('Diagnostic AI report generation failed: ' . $exception->getMessage()); + } + + return [ + 'copy' => $defaults, + 'source' => 'fallback', + ]; +} + +function diagnostic_build_report_html(array $attempt, array $copy): string { $result = $attempt['result'] ?? []; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $resultUrl = $scheme . '://' . $host . '/diagnostic.php?view=result'; - $priorityCards = ''; - foreach (($result['weakest_areas'] ?? []) as $area) { - $priorityCards .= '
' - . '
Priorytet
' - . '
' . diagnostic_email_escape($area['section_name'] ?? '') . '
' - . '
' . (int)($area['percentage'] ?? 0) . '% dojrzałości — ten obszar najczęściej wymaga najszybszego uporządkowania.
' - . '
'; - } - if ($priorityCards === '') { - $priorityCards = '

Brak danych o priorytetowych obszarach do pokazania w tym raporcie.

'; - } + $strengthsHtml = diagnostic_email_list($copy['strengths'] ?? []); + $prioritiesHtml = diagnostic_email_list($copy['priorities'] ?? []); + $nextStepsHtml = diagnostic_email_list($copy['next_steps'] ?? []); - $recommendations = ''; - foreach (($result['recommendations'] ?? []) as $item) { - $recommendations .= '
  • ' . diagnostic_email_escape((string)$item) . '
  • '; - } - if ($recommendations === '') { - $recommendations = '
  • Brak dodatkowych rekomendacji.
  • '; - } - - $content = '

    Dziękujemy za wypełnienie diagnozy. Obecny poziom dojrzałości operacyjnej Twojej firmy to ' . diagnostic_email_escape($result['segment_label'] ?? 'Diagnoza zakończona') . '.

    ' + $content = '

    ' . diagnostic_email_escape((string)($copy['intro'] ?? '')) . '

    ' . '' . '' . '' . '' . '
    ' . '
    Wynik łączny
    ' - . '
    ' . (int)($result['percentage_score'] ?? 0) . '%
    ' - . '
    ' . diagnostic_email_escape($result['segment_summary'] ?? '') . '
    ' + . '
    ' . (int)($result['percentage_score'] ?? 0) . '%
    ' + . '
    ' . diagnostic_email_escape((string)($result['segment_label'] ?? 'Diagnoza zakończona')) . '
    ' + . '
    ' . diagnostic_email_escape((string)($copy['executive_summary'] ?? '')) . '
    ' . '
    ' - . '

    Priorytetowe obszary do poprawy

    ' - . $priorityCards + . '
    ' + . '
    Co ten wynik oznacza
    ' + . '
    ' . diagnostic_email_escape((string)($copy['what_it_means'] ?? '')) . '
    ' + . '
    ' + . '

    Mocne strony

    ' + . $strengthsHtml + . '

    Priorytety na teraz

    ' + . $prioritiesHtml . '

    Rekomendowane kolejne kroki

    ' - . '' + . $nextStepsHtml . '
    ' - . '
    Chcesz omówić wynik z zespołem?
    ' - . '
    Wróć do podsumowania diagnozy, aby ustalić priorytety zmian i zamówić kolejny krok.
    ' + . '
    ' . diagnostic_email_escape((string)($copy['closing'] ?? '')) . '
    ' + . '
    W podsumowaniu online zobaczysz pełen wynik i możesz od razu zostawić numer do kontaktu.
    ' . 'Otwórz podsumowanie diagnozy' . '
    '; return diagnostic_email_document( - 'Raport z diagnozy dojrzałości procesowej', - 'Twój wynik diagnozy i rekomendowane kolejne kroki są gotowe.', + (string)($copy['subject'] ?? 'Raport z diagnozy dojrzałości procesowej'), + (string)($copy['preheader'] ?? 'Twój wynik diagnozy i rekomendowane kolejne kroki są gotowe.'), $content ); } -function diagnostic_build_report_text(array $attempt): string +function diagnostic_build_report_text(array $attempt, array $copy): string { $result = $attempt['result'] ?? []; $lines = []; - $lines[] = 'Raport z diagnozy dojrzałości procesowej'; + $lines[] = (string)($copy['subject'] ?? 'Raport z diagnozy dojrzałości procesowej'); $lines[] = 'Wynik: ' . ($result['segment_label'] ?? 'Diagnoza zakończona') . ' (' . (int)($result['percentage_score'] ?? 0) . '%)'; - $lines[] = (string)($result['segment_summary'] ?? ''); $lines[] = ''; - $lines[] = 'Priorytetowe obszary:'; - foreach (($result['weakest_areas'] ?? []) as $area) { - $lines[] = '- ' . $area['section_name'] . ': ' . (int)$area['percentage'] . '%'; - } + $lines[] = (string)($copy['intro'] ?? ''); $lines[] = ''; - $lines[] = 'Rekomendowane działania:'; - foreach (($result['recommendations'] ?? []) as $item) { + $lines[] = (string)($copy['executive_summary'] ?? ''); + $lines[] = ''; + $lines[] = 'Co ten wynik oznacza:'; + $lines[] = (string)($copy['what_it_means'] ?? ''); + $lines[] = ''; + $lines[] = 'Mocne strony:'; + foreach (($copy['strengths'] ?? []) as $item) { $lines[] = '- ' . $item; } - return implode("\n", $lines); + $lines[] = ''; + $lines[] = 'Priorytety na teraz:'; + foreach (($copy['priorities'] ?? []) as $item) { + $lines[] = '- ' . $item; + } + $lines[] = ''; + $lines[] = 'Rekomendowane kolejne kroki:'; + foreach (($copy['next_steps'] ?? []) as $item) { + $lines[] = '- ' . $item; + } + $lines[] = ''; + $lines[] = (string)($copy['closing'] ?? ''); + return diagnostic_report_limit(implode("\n", $lines), 10000); } -function diagnostic_send_report_email(array $attempt): array +function diagnostic_store_report_content(int $attemptId, array $package): void { - if (empty($attempt['email']) || empty($attempt['result'])) { - return ['success' => false, 'error' => 'Brak adresu e-mail lub wyniku próby']; + $stmt = db()->prepare( + 'UPDATE diagnostic_attempts + SET report_subject = :report_subject, + report_html = :report_html, + report_text = :report_text, + report_generated_at = NOW(), + updated_at = NOW() + WHERE id = :id' + ); + $stmt->execute([ + ':report_subject' => (string)($package['subject'] ?? ''), + ':report_html' => (string)($package['html'] ?? ''), + ':report_text' => (string)($package['text'] ?? ''), + ':id' => $attemptId, + ]); +} + +function diagnostic_cached_report_content(array $attempt): ?array +{ + $subject = diagnostic_report_limit((string)($attempt['report_subject'] ?? ''), 255); + $html = (string)($attempt['report_html'] ?? ''); + $text = (string)($attempt['report_text'] ?? ''); + + if ($subject === '' || $html === '' || $text === '') { + return null; } - $subject = 'Raport z diagnozy dojrzałości procesowej'; - $html = diagnostic_build_report_html($attempt); - $text = diagnostic_build_report_text($attempt); - return MailService::sendMail($attempt['email'], $subject, $html, $text); + return [ + 'success' => true, + 'subject' => $subject, + 'html' => $html, + 'text' => $text, + 'source' => 'cache', + ]; +} + +function diagnostic_generate_report_content(array $attempt): array +{ + if (empty($attempt['result'])) { + return ['success' => false, 'error' => 'Brak wyniku potrzebnego do wygenerowania raportu.']; + } + + $ai = diagnostic_generate_ai_report_copy($attempt); + $copy = $ai['copy'] ?? diagnostic_default_report_copy($attempt); + + return [ + 'success' => true, + 'subject' => (string)($copy['subject'] ?? 'Raport z diagnozy dojrzałości procesowej'), + 'html' => diagnostic_build_report_html($attempt, $copy), + 'text' => diagnostic_build_report_text($attempt, $copy), + 'source' => (string)($ai['source'] ?? 'fallback'), + ]; +} + +function diagnostic_resolve_report_content(array $attempt, bool $forceRegenerate = false): array +{ + if (!$forceRegenerate) { + $cached = diagnostic_cached_report_content($attempt); + if ($cached !== null) { + return $cached; + } + } + + $generated = diagnostic_generate_report_content($attempt); + if (!empty($generated['success']) && !empty($attempt['id'])) { + diagnostic_store_report_content((int)$attempt['id'], $generated); + } + + return $generated; +} + +function diagnostic_send_report_email(array $attempt, bool $forceRegenerate = false): array +{ + if (empty($attempt['email']) || !filter_var((string)$attempt['email'], FILTER_VALIDATE_EMAIL)) { + return ['success' => false, 'error' => 'Brak poprawnego adresu e-mail klienta']; + } + + $report = diagnostic_resolve_report_content($attempt, $forceRegenerate); + if (empty($report['success'])) { + return $report; + } + + return MailService::sendMail( + $attempt['email'], + (string)$report['subject'], + (string)$report['html'], + (string)$report['text'] + ); } function diagnostic_admin_setting_get(string $key): ?string