feat: Enhance MailToTaskImporter with AI error handling and update OpenAI model to gpt-4o-mini
feat: Add Gantt task normalization and filter selection in main_view.php
This commit is contained in:
159
.vscode/ftp-kr.sync.cache.json
vendored
159
.vscode/ftp-kr.sync.cache.json
vendored
@@ -40,6 +40,92 @@
|
|||||||
"lmtime": 0,
|
"lmtime": 0,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Users": {
|
||||||
|
"UserRepository.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 610,
|
||||||
|
"lmtime": 1770653480229,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"factory": {
|
||||||
|
"class.BackendSites.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 2962,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"class.Crm.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1863,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"class.Cron.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 26120,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"class.Finances.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 16159,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"class.Projects.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 27476,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"class.Tasks.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 20921,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"class.Users.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 2072,
|
||||||
|
"lmtime": 1770654026945,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"class.Wiki.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1911,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Controllers": {
|
||||||
|
"class.TasksController.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 567,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"TasksController.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 3009,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"UsersController.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 3914,
|
||||||
|
"lmtime": 1770653696575,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"class.Users.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 4242,
|
||||||
|
"lmtime": 1770653518273,
|
||||||
|
"modified": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -57,6 +143,12 @@
|
|||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CODE_INDEX.md": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 16884,
|
||||||
|
"lmtime": 1770652965090,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
"config.php": {
|
"config.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 1249,
|
"size": 1249,
|
||||||
@@ -140,9 +232,74 @@
|
|||||||
"lmtime": 0,
|
"lmtime": 0,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"login-form.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 3336,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"main-view.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 2012,
|
||||||
|
"lmtime": 1770653623175,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"settings.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 3457,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"layout-cron.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 6369,
|
||||||
|
"lmtime": 1770653884637,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"layout-logged.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 6762,
|
||||||
|
"lmtime": 1770653877600,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"layout-unlogged.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 986,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tests": {
|
||||||
|
"Controllers": {
|
||||||
|
"UsersControllerTest.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1839,
|
||||||
|
"lmtime": 1770653599232,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Domain": {
|
||||||
|
"Users": {
|
||||||
|
"UserRepositoryTest.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 1540,
|
||||||
|
"lmtime": 1770653587539,
|
||||||
|
"modified": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 851,
|
||||||
|
"lmtime": 1770653605021,
|
||||||
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tests": {},
|
|
||||||
"tmp_debug_mail_import.php": {
|
"tmp_debug_mail_import.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 1238,
|
"size": 1238,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class MailToTaskImporter
|
|||||||
|
|
||||||
private $mdb;
|
private $mdb;
|
||||||
private $attachments;
|
private $attachments;
|
||||||
|
private $last_ai_error;
|
||||||
|
|
||||||
public function __construct( $mdb = null )
|
public function __construct( $mdb = null )
|
||||||
{
|
{
|
||||||
@@ -22,6 +23,7 @@ class MailToTaskImporter
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this -> attachments = new TaskAttachmentRepository( $this -> mdb );
|
$this -> attachments = new TaskAttachmentRepository( $this -> mdb );
|
||||||
|
$this -> last_ai_error = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function importFromImap( array $config )
|
public function importFromImap( array $config )
|
||||||
@@ -88,24 +90,38 @@ class MailToTaskImporter
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
$content = $this -> extractMessageContent( $imap, $message_no );
|
$content = $this -> extractMessageContent( $imap, $message_no );
|
||||||
|
$ai_used = false;
|
||||||
|
$ai_error_for_log = null;
|
||||||
|
|
||||||
// Sprawdź czy użyć AI do parsowania
|
// Sprawdź czy użyć AI do parsowania
|
||||||
global $settings;
|
global $settings;
|
||||||
$use_ai = isset( $settings['openai_parse_emails'] ) && $settings['openai_parse_emails'] === true;
|
$use_ai = isset( $settings['openai_parse_emails'] ) && $settings['openai_parse_emails'] === true;
|
||||||
$api_key = isset( $settings['openai_api_key'] ) ? trim( (string)$settings['openai_api_key'] ) : '';
|
$api_key = isset( $settings['openai_api_key'] ) ? trim( (string)$settings['openai_api_key'] ) : '';
|
||||||
|
if ( $use_ai && $api_key === '' )
|
||||||
|
{
|
||||||
|
$ai_error_for_log = 'Brak klucza API OpenAI.';
|
||||||
|
error_log( '[MailToTaskImporter] AI fallback: ' . $ai_error_for_log );
|
||||||
|
}
|
||||||
|
|
||||||
if ( $use_ai && $api_key !== '' )
|
if ( $use_ai && $api_key !== '' )
|
||||||
{
|
{
|
||||||
$model = isset( $settings['openai_model'] ) ? trim( (string)$settings['openai_model'] ) : 'gpt-3.5-turbo';
|
$model = isset( $settings['openai_model'] ) ? trim( (string)$settings['openai_model'] ) : 'gpt-4o-mini';
|
||||||
$ai_result = $this -> parseWithAI( $api_key, $model, $subject, $content['text'] );
|
$ai_result = $this -> parseWithAI( $api_key, $model, $subject, $content['text'] );
|
||||||
if ( $ai_result !== null )
|
if ( is_array( $ai_result ) && isset( $ai_result['task_name'] ) && isset( $ai_result['task_text'] ) )
|
||||||
{
|
{
|
||||||
$task_name = $ai_result['task_name'];
|
$task_name = $ai_result['task_name'];
|
||||||
$task_text = $ai_result['task_text'];
|
$task_text = $ai_result['task_text'];
|
||||||
|
$ai_used = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback do normalnego parsowania jeśli AI nie zadziała
|
// Fallback do normalnego parsowania jeśli AI nie zadziała
|
||||||
|
if ( $this -> last_ai_error !== '' )
|
||||||
|
{
|
||||||
|
error_log( '[MailToTaskImporter] AI fallback: ' . $this -> last_ai_error );
|
||||||
|
$ai_error_for_log = $this -> last_ai_error;
|
||||||
|
}
|
||||||
|
|
||||||
$task_name = trim( $subject ) !== '' ? trim( $subject ) : '(bez tematu)';
|
$task_name = trim( $subject ) !== '' ? trim( $subject ) : '(bez tematu)';
|
||||||
$task_text = $this -> prepareImportedTaskText( $content['text'] );
|
$task_text = $this -> prepareImportedTaskText( $content['text'] );
|
||||||
}
|
}
|
||||||
@@ -161,7 +177,17 @@ class MailToTaskImporter
|
|||||||
$this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $attachment['name'], $attachment['content'] );
|
$this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $attachment['name'], $attachment['content'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
$this -> saveImportLog( $message_key, $task_id, $sender, $subject, 'imported', null );
|
$import_status = 'imported';
|
||||||
|
$import_error = null;
|
||||||
|
if ( $use_ai && $ai_used )
|
||||||
|
$import_status = 'imported_ai';
|
||||||
|
elseif ( $use_ai && !$ai_used )
|
||||||
|
{
|
||||||
|
$import_status = 'imported_fallback';
|
||||||
|
$import_error = $ai_error_for_log;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this -> saveImportLog( $message_key, $task_id, $sender, $subject, $import_status, $import_error );
|
||||||
$imported++;
|
$imported++;
|
||||||
}
|
}
|
||||||
catch ( \Throwable $e )
|
catch ( \Throwable $e )
|
||||||
@@ -173,7 +199,7 @@ class MailToTaskImporter
|
|||||||
}
|
}
|
||||||
|
|
||||||
$status_tmp = $this -> getImportStatus( $message_key );
|
$status_tmp = $this -> getImportStatus( $message_key );
|
||||||
if ( in_array( $status_tmp, [ 'imported', 'skipped_sender' ], true ) )
|
if ( in_array( $status_tmp, [ 'imported', 'imported_ai', 'imported_fallback', 'skipped_sender' ], true ) )
|
||||||
@imap_delete( $imap, $message_no );
|
@imap_delete( $imap, $message_no );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +344,7 @@ class MailToTaskImporter
|
|||||||
private function isMessageFinalized( $message_key )
|
private function isMessageFinalized( $message_key )
|
||||||
{
|
{
|
||||||
$status = $this -> getImportStatus( $message_key );
|
$status = $this -> getImportStatus( $message_key );
|
||||||
return in_array( $status, [ 'imported', 'skipped_sender' ], true );
|
return in_array( $status, [ 'imported', 'imported_ai', 'imported_fallback', 'skipped_sender' ], true );
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getImportStatus( $message_key )
|
private function getImportStatus( $message_key )
|
||||||
@@ -675,14 +701,34 @@ class MailToTaskImporter
|
|||||||
return strtolower( $value );
|
return strtolower( $value );
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseWithAI( $api_key, $model, $subject, $raw_content )
|
private function parseWithAI( $api_key, $model, $subject, $raw_content, $attempt = 1 )
|
||||||
{
|
{
|
||||||
|
$this -> last_ai_error = '';
|
||||||
|
$api_key = trim( (string)$api_key );
|
||||||
$subject = trim( (string)$subject );
|
$subject = trim( (string)$subject );
|
||||||
$raw_content = trim( (string)$raw_content );
|
$raw_content = trim( (string)$raw_content );
|
||||||
$model = trim( (string)$model );
|
$model = trim( (string)$model );
|
||||||
|
|
||||||
|
if ( $api_key === '' )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'Brak klucza API OpenAI.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if ( $model === '' )
|
if ( $model === '' )
|
||||||
$model = 'gpt-3.5-turbo';
|
$model = 'gpt-4o-mini';
|
||||||
|
|
||||||
|
if ( $raw_content === '' )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'Pusta tresc emaila do analizy AI.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !function_exists( 'curl_init' ) )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'Brak rozszerzenia cURL.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Przygotuj treść do analizy (usuń HTML jesli jest)
|
// Przygotuj treść do analizy (usuń HTML jesli jest)
|
||||||
if ( preg_match( '/<\s*[a-z][^>]*>/i', $raw_content ) )
|
if ( preg_match( '/<\s*[a-z][^>]*>/i', $raw_content ) )
|
||||||
@@ -691,31 +737,54 @@ class MailToTaskImporter
|
|||||||
$raw_content = $this -> cleanBodyText( $raw_content );
|
$raw_content = $this -> cleanBodyText( $raw_content );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$content_limit = $attempt > 1 ? 1200 : 1800;
|
||||||
$prompt = "Jesteś asystentem do przetwarzania emaili na zadania w systemie CRM. " .
|
$prompt = "Jesteś asystentem do przetwarzania emaili na zadania w systemie CRM. " .
|
||||||
"Na podstawie poniższego tematu i treści emaila, wygeneruj:\n" .
|
"Na podstawie poniższego tematu i treści emaila wygeneruj zadanie wdrożeniowe, NIE podsumowanie. " .
|
||||||
"1. Zwięzły, konkretny temat zadania (max 100 znaków)\n" .
|
"Opis ma być szczegółową listą rzeczy do wykonania dla zespołu.\n\n" .
|
||||||
"2. Czytelny opis zadania zawierający najważniejsze informacje\n\n" .
|
"Wymagania:\n" .
|
||||||
|
"- Nie pomijaj żadnych wymagań, liczb, zakresów cen, CTA, pól formularza, kroków i terminów.\n" .
|
||||||
|
"- Zamień treść emaila na checklistę implementacyjną.\n" .
|
||||||
|
"- Zachowaj strukturę sekcji i podsekcji.\n" .
|
||||||
|
"- Pisz konkretnie co zmienić, gdzie zmienić i jakie treści dodać.\n" .
|
||||||
|
"- Nie używaj ogólników typu: \"zaktualizować stronę\".\n\n" .
|
||||||
|
"Format pola task_text:\n" .
|
||||||
|
"1) Cel zmiany\n" .
|
||||||
|
"2) Zakres zmian (lista punktów)\n" .
|
||||||
|
"3) Treści do wstawienia (dokładne teksty/liczby)\n" .
|
||||||
|
"4) Zmiany formularza\n" .
|
||||||
|
"5) Wersja sezonowa / warunki wyświetlania\n" .
|
||||||
|
"6) CTA i bonusy\n" .
|
||||||
|
"7) Kryteria akceptacji\n\n" .
|
||||||
"Temat emaila: " . $subject . "\n\n" .
|
"Temat emaila: " . $subject . "\n\n" .
|
||||||
"Treść emaila:\n" . mb_substr( $raw_content, 0, 2000 ) . "\n\n" .
|
"Treść emaila:\n" . mb_substr( $raw_content, 0, $content_limit ) . "\n\n" .
|
||||||
"Odpowiedz TYLKO w formacie JSON bez żadnych dodatkowych wyjaśnień:\n" .
|
"Zwróć TYLKO poprawny JSON:\n" .
|
||||||
'{"task_name": "temat zadania", "task_text": "opis zadania"}';
|
'{"task_name": "krótki temat zadania", "task_text": "szczegółowa checklista wdrożeniowa"}';
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
'messages' => [
|
'messages' => [
|
||||||
[
|
[
|
||||||
'role' => 'system',
|
'role' => 'system',
|
||||||
'content' => 'Jesteś asystentem do przetwarzania emaili. Odpowiadasz TYLKO w formacie JSON.'
|
'content' => 'Jesteś asystentem do przetwarzania emaili. Odpowiadasz TYLKO w formacie JSON i tworzysz szczegółowe checklisty wdrożeniowe, nie skróty.'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'role' => 'user',
|
'role' => 'user',
|
||||||
'content' => $prompt
|
'content' => $prompt
|
||||||
]
|
]
|
||||||
],
|
]
|
||||||
'temperature' => 0.3,
|
|
||||||
'max_tokens' => 500
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ( stripos( $model, 'gpt-5' ) === 0 )
|
||||||
|
$payload['response_format'] = [ 'type' => 'json_object' ];
|
||||||
|
|
||||||
|
if ( stripos( $model, 'gpt-5' ) === 0 )
|
||||||
|
$payload['max_completion_tokens'] = $attempt > 1 ? 1600 : 1000;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$payload['temperature'] = 0.3;
|
||||||
|
$payload['max_tokens'] = 1200;
|
||||||
|
}
|
||||||
|
|
||||||
$ch = curl_init( 'https://api.openai.com/v1/chat/completions' );
|
$ch = curl_init( 'https://api.openai.com/v1/chat/completions' );
|
||||||
curl_setopt_array( $ch, [
|
curl_setopt_array( $ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
@@ -729,28 +798,87 @@ class MailToTaskImporter
|
|||||||
] );
|
] );
|
||||||
|
|
||||||
$response = curl_exec( $ch );
|
$response = curl_exec( $ch );
|
||||||
|
$curl_error = curl_error( $ch );
|
||||||
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||||
curl_close( $ch );
|
curl_close( $ch );
|
||||||
|
|
||||||
if ( $http_code !== 200 || !$response )
|
if ( $response === false )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'Blad cURL: ' . $curl_error;
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $http_code !== 200 )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'HTTP ' . $http_code . ' z OpenAI: ' . $this -> extractApiErrorMessage( $response );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !$response )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'Pusta odpowiedz z OpenAI.';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$data = json_decode( $response, true );
|
$data = json_decode( $response, true );
|
||||||
if ( !isset( $data['choices'][0]['message']['content'] ) )
|
if ( !is_array( $data ) )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'Niepoprawny JSON z OpenAI: ' . json_last_error_msg();
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$content = trim( $data['choices'][0]['message']['content'] );
|
if ( isset( $data['error']['message'] ) )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'OpenAI error: ' . (string)$data['error']['message'];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !isset( $data['choices'][0] ) || !is_array( $data['choices'][0] ) )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'Brak choices[0] w odpowiedzi OpenAI. RAW: ' . $this -> clipForLog( $response );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$choice = $data['choices'][0];
|
||||||
|
$content = $this -> extractModelMessageContent( $choice );
|
||||||
|
if ( $content === '' )
|
||||||
|
{
|
||||||
|
$finish_reason = isset( $choice['finish_reason'] ) ? (string)$choice['finish_reason'] : '(brak)';
|
||||||
|
$refusal = isset( $choice['message']['refusal'] ) ? (string)$choice['message']['refusal'] : '';
|
||||||
|
|
||||||
|
if ( $finish_reason === 'length' && (int)$attempt === 1 )
|
||||||
|
{
|
||||||
|
$retry_content = mb_substr( $raw_content, 0, 1200 );
|
||||||
|
return $this -> parseWithAI( $api_key, $model, $subject, $retry_content, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = 'Pusta tresc odpowiedzi modelu. finish_reason=' . $finish_reason;
|
||||||
|
if ( $refusal !== '' )
|
||||||
|
$error .= ', refusal=' . $this -> clipForLog( $refusal );
|
||||||
|
|
||||||
|
$this -> last_ai_error = $error . '. RAW: ' . $this -> clipForLog( $response );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Usuń markdown code block jeśli jest
|
// Usuń markdown code block jeśli jest
|
||||||
$content = preg_replace( '/```json\s*|\s*```/', '', $content );
|
$content = preg_replace( '/```json\s*|\s*```/', '', $content );
|
||||||
$content = trim( $content );
|
$content = trim( $content );
|
||||||
|
|
||||||
|
// Jeśli model zwrócił JSON z komentarzem wokół, wyciągnij tylko obiekt.
|
||||||
|
$json_start = strpos( $content, '{' );
|
||||||
|
$json_end = strrpos( $content, '}' );
|
||||||
|
if ( $json_start !== false && $json_end !== false && $json_end > $json_start )
|
||||||
|
$content = substr( $content, $json_start, $json_end - $json_start + 1 );
|
||||||
|
|
||||||
$parsed = json_decode( $content, true );
|
$parsed = json_decode( $content, true );
|
||||||
if ( !is_array( $parsed ) || !isset( $parsed['task_name'] ) || !isset( $parsed['task_text'] ) )
|
if ( !is_array( $parsed ) || !isset( $parsed['task_name'] ) || !isset( $parsed['task_text'] ) )
|
||||||
|
{
|
||||||
|
$this -> last_ai_error = 'Niepoprawny JSON z modelu. Odpowiedz: ' . $this -> clipForLog( $content );
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$task_name = trim( (string)$parsed['task_name'] );
|
$task_name = $this -> normalizeAiTextValue( $parsed['task_name'] );
|
||||||
$task_text = trim( (string)$parsed['task_text'] );
|
$task_text = $this -> normalizeAiTextValue( $parsed['task_text'] );
|
||||||
|
|
||||||
if ( $task_name === '' )
|
if ( $task_name === '' )
|
||||||
$task_name = '(bez tematu)';
|
$task_name = '(bez tematu)';
|
||||||
@@ -766,4 +894,92 @@ class MailToTaskImporter
|
|||||||
'task_text' => $task_text
|
'task_text' => $task_text
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function extractApiErrorMessage( $response )
|
||||||
|
{
|
||||||
|
$data = json_decode( (string)$response, true );
|
||||||
|
if ( is_array( $data ) && isset( $data['error']['message'] ) )
|
||||||
|
return (string)$data['error']['message'];
|
||||||
|
|
||||||
|
return $this -> clipForLog( $response );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractModelMessageContent( array $choice )
|
||||||
|
{
|
||||||
|
if ( isset( $choice['message']['content'] ) && is_string( $choice['message']['content'] ) )
|
||||||
|
return trim( $choice['message']['content'] );
|
||||||
|
|
||||||
|
if ( isset( $choice['message']['content'] ) && is_array( $choice['message']['content'] ) )
|
||||||
|
{
|
||||||
|
$chunks = [];
|
||||||
|
foreach ( $choice['message']['content'] as $part )
|
||||||
|
{
|
||||||
|
if ( is_string( $part ) )
|
||||||
|
$chunks[] = $part;
|
||||||
|
elseif ( is_array( $part ) )
|
||||||
|
{
|
||||||
|
if ( isset( $part['text'] ) && is_string( $part['text'] ) )
|
||||||
|
$chunks[] = $part['text'];
|
||||||
|
elseif ( isset( $part['text']['value'] ) && is_string( $part['text']['value'] ) )
|
||||||
|
$chunks[] = $part['text']['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim( implode( "\n", $chunks ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $choice['message']['tool_calls'][0]['function']['arguments'] ) && is_string( $choice['message']['tool_calls'][0]['function']['arguments'] ) )
|
||||||
|
return trim( $choice['message']['tool_calls'][0]['function']['arguments'] );
|
||||||
|
|
||||||
|
if ( isset( $choice['message']['function_call']['arguments'] ) && is_string( $choice['message']['function_call']['arguments'] ) )
|
||||||
|
return trim( $choice['message']['function_call']['arguments'] );
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clipForLog( $value, $limit = 600 )
|
||||||
|
{
|
||||||
|
$text = trim( (string)$value );
|
||||||
|
if ( $text === '' )
|
||||||
|
return '';
|
||||||
|
|
||||||
|
return mb_substr( $text, 0, (int)$limit );
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeAiTextValue( $value )
|
||||||
|
{
|
||||||
|
if ( is_string( $value ) )
|
||||||
|
return trim( $value );
|
||||||
|
|
||||||
|
if ( is_numeric( $value ) || is_bool( $value ) )
|
||||||
|
return trim( (string)$value );
|
||||||
|
|
||||||
|
if ( is_array( $value ) )
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
foreach ( $value as $item )
|
||||||
|
{
|
||||||
|
$item_text = $this -> normalizeAiTextValue( $item );
|
||||||
|
if ( $item_text !== '' )
|
||||||
|
$lines[] = $item_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim( implode( "\n", $lines ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_object( $value ) )
|
||||||
|
{
|
||||||
|
$array_value = json_decode( json_encode( $value ), true );
|
||||||
|
if ( is_array( $array_value ) )
|
||||||
|
{
|
||||||
|
$normalized = $this -> normalizeAiTextValue( $array_value );
|
||||||
|
if ( $normalized !== '' )
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim( json_encode( $value ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class Tasks
|
|||||||
|
|
||||||
static public function get_tasks_gantt( $user_id, $projects = null, $users = null ) {
|
static public function get_tasks_gantt( $user_id, $projects = null, $users = null ) {
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
$data = [];
|
||||||
|
|
||||||
if ( $users ) {
|
if ( $users ) {
|
||||||
$sql = ' AND id IN (SELECT task_id FROM task_user WHERE user_id IN (' . implode( ',', $users ) . ')) ';
|
$sql = ' AND id IN (SELECT task_id FROM task_user WHERE user_id IN (' . implode( ',', $users ) . ')) ';
|
||||||
@@ -95,6 +96,7 @@ class Tasks
|
|||||||
// custom class
|
// custom class
|
||||||
// if ( $task['date_start'] <= date( 'Y-m-d H:i:s' ) )
|
// if ( $task['date_start'] <= date( 'Y-m-d H:i:s' ) )
|
||||||
// $task_json['custom_class'] = 'gantt-task-backlog';
|
// $task_json['custom_class'] = 'gantt-task-backlog';
|
||||||
|
$task_json['custom_class'] = '';
|
||||||
|
|
||||||
if ( $task['parent_id'] )
|
if ( $task['parent_id'] )
|
||||||
$task_json['dependencies'] = $task['parent_id'];
|
$task_json['dependencies'] = $task['parent_id'];
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ $settings['tasks_auto_start_timer'] = false;
|
|||||||
// OpenAI ChatGPT API configuration for email task parsing
|
// OpenAI ChatGPT API configuration for email task parsing
|
||||||
$settings['openai_api_key'] = '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'; // Wklej tutaj swój klucz API OpenAI
|
$settings['openai_api_key'] = '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'; // Wklej tutaj swój klucz API OpenAI
|
||||||
$settings['openai_parse_emails'] = true; // true = użyj AI do parsowania emaili, false = normalne parsowanie
|
$settings['openai_parse_emails'] = true; // true = użyj AI do parsowania emaili, false = normalne parsowanie
|
||||||
$settings['openai_model'] = 'gpt-3.5-turbo'; // Model: gpt-3.5-turbo (najtańszy), gpt-4o-mini, gpt-4o, itp.
|
$settings['openai_model'] = 'gpt-4o-mini'; // Model: gpt-4o-mini, gpt-4o, gpt-5-nano, itp.
|
||||||
|
|||||||
@@ -156,11 +156,13 @@
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
];
|
];
|
||||||
if ( tasks.length <= 0 ) {
|
|
||||||
tasks = [
|
function getEmptyGanttTasks()
|
||||||
|
{
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
start: new Date().toISOString().split('T')[0],
|
start: new Date().toISOString().split( 'T' )[0],
|
||||||
end: new Date().toISOString().split('T')[0],
|
end: new Date().toISOString().split( 'T' )[0],
|
||||||
name: "Brak zadań do wyświetlenia",
|
name: "Brak zadań do wyświetlenia",
|
||||||
id: "0",
|
id: "0",
|
||||||
progress: 100,
|
progress: 100,
|
||||||
@@ -169,6 +171,29 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeGanttTasks( tasksData )
|
||||||
|
{
|
||||||
|
if ( Array.isArray( tasksData ) && tasksData.length > 0 )
|
||||||
|
return tasksData;
|
||||||
|
|
||||||
|
return getEmptyGanttTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedTaskFilters()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
projects: jQuery( 'input[name="projects"].g-checkbox:checked' ).map(function() {
|
||||||
|
return this.value;
|
||||||
|
}).get(),
|
||||||
|
users: jQuery( 'input[name="users"].g-checkbox:checked' ).map(function() {
|
||||||
|
return this.value;
|
||||||
|
}).get()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks = normalizeGanttTasks( tasks );
|
||||||
|
|
||||||
var gantt_chart = new Gantt(".gantt-target", tasks, {
|
var gantt_chart = new Gantt(".gantt-target", tasks, {
|
||||||
on_click: function (task) {
|
on_click: function (task) {
|
||||||
console.log(task);
|
console.log(task);
|
||||||
@@ -234,8 +259,7 @@
|
|||||||
$( '.tasks_container .tasks_suspended ul' ).empty().append( data.tasks_suspended );
|
$( '.tasks_container .tasks_suspended ul' ).empty().append( data.tasks_suspended );
|
||||||
$( '.tasks_container .tasks_to_do ul' ).empty().append( data.tasks_to_do );
|
$( '.tasks_container .tasks_to_do ul' ).empty().append( data.tasks_to_do );
|
||||||
$( '.tasks_container .tasks_fvat ul' ).empty().append( data.tasks_fvat );
|
$( '.tasks_container .tasks_fvat ul' ).empty().append( data.tasks_fvat );
|
||||||
if ( data.tasks_gantt )
|
gantt_chart.refresh( normalizeGanttTasks( data.tasks_gantt ) );
|
||||||
gantt_chart.refresh( data.tasks_gantt );
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1156,10 +1180,8 @@
|
|||||||
var data = jQuery.parseJSON( response );
|
var data = jQuery.parseJSON( response );
|
||||||
if ( data.status == 'success' )
|
if ( data.status == 'success' )
|
||||||
{
|
{
|
||||||
var checkedVals = jQuery( 'input.g-checkbox:checked' ).map(function() {
|
var selectedFilters = getSelectedTaskFilters();
|
||||||
return this.value;
|
reload_tasks( selectedFilters.projects, selectedFilters.users );
|
||||||
}).get();
|
|
||||||
reload_tasks( checkedVals );
|
|
||||||
close_task_popup();
|
close_task_popup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user