Files
crmPRO/autoload/Domain/Tasks/TaskTitleGenerator.php
Codex 3b1ba1b5ed feat(06-task-title-ai): complete OpenAI task title suggestions
Phase 6 complete:

- add AI title generator service using gpt-5-nano

- add task popup button for biuro@project-pro.pl

- add AJAX endpoint returning title suggestions without auto-save
2026-05-04 23:45:54 +02:00

191 lines
5.5 KiB
PHP

<?php
namespace Domain\Tasks;
class TaskTitleGenerator
{
private const ENDPOINT = 'https://api.openai.com/v1/chat/completions';
private const CONTENT_LIMIT = 3000;
private const TITLE_LIMIT = 120;
public function generate( $api_key, $model, $task_text )
{
$api_key = trim( (string)$api_key );
$model = trim( (string)$model );
$task_text = $this -> prepareTaskText( $task_text );
if ( $api_key === '' )
return $this -> error( 'Brak klucza API OpenAI.' );
if ( $model === '' )
$model = 'gpt-5-nano';
if ( $task_text === '' )
return $this -> error( 'Brak tresci zadania do wygenerowania tytulu.' );
if ( !function_exists( 'curl_init' ) )
return $this -> error( 'Brak rozszerzenia cURL.' );
$payload = $this -> buildPayload( $model, $task_text );
$response = $this -> sendRequest( $api_key, $payload );
if ( $response['status'] !== 'success' )
return $response;
$title = $this -> extractTitle( $response['body'] );
if ( $title === '' )
return $this -> error( 'OpenAI nie zwrocil poprawnego tytulu.' );
return [
'status' => 'success',
'title' => $title
];
}
private function buildPayload( $model, $task_text )
{
$payload = [
'model' => $model,
'messages' => [
[
'role' => 'system',
'content' => 'Tworzysz bardzo krotkie, bezosobowe tytuly zadan w CRM. Odpowiadasz tylko tytulem, bez komentarza.'
],
[
'role' => 'user',
'content' => "Na podstawie tresci zadania zaproponuj jeden skrocony tytul po polsku.\n" .
"Wymagania:\n" .
"- maksymalnie 6 slow,\n" .
"- forma bezosobowa rzeczownikowa, np. \"Usuniecie bloku o firmie\",\n" .
"- bez trybu rozkazujacego i bez form typu \"usun\", \"dodaj\", \"popraw\",\n" .
"- bez cudzyslowow,\n" .
"- bez kropki na koncu,\n" .
"- bez ogolnikow typu \"zadanie\" lub \"sprawa\".\n\n" .
"Tresc zadania:\n" . $task_text
]
]
];
if ( stripos( $model, 'gpt-5' ) === 0 )
{
$payload['reasoning_effort'] = 'minimal';
$payload['max_completion_tokens'] = 500;
}
else
{
$payload['temperature'] = 0.2;
$payload['max_tokens'] = 80;
}
return $payload;
}
private function sendRequest( $api_key, array $payload )
{
$ch = curl_init( self::ENDPOINT );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode( $payload ),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key
],
CURLOPT_TIMEOUT => 20
] );
$body = curl_exec( $ch );
$curl_error = curl_error( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( $body === false )
return $this -> error( 'Blad polaczenia z OpenAI: ' . $curl_error );
if ( (int)$http_code !== 200 )
return $this -> error( 'OpenAI zwrocil blad HTTP ' . (int)$http_code . ': ' . $this -> extractApiErrorMessage( $body ) );
if ( trim( (string)$body ) === '' )
return $this -> error( 'Pusta odpowiedz z OpenAI.' );
return [
'status' => 'success',
'body' => (string)$body
];
}
private function extractTitle( $response_body )
{
$data = json_decode( (string)$response_body, true );
if ( !is_array( $data ) )
return '';
if ( isset( $data['error']['message'] ) )
return '';
$content = '';
if ( isset( $data['choices'][0]['message']['content'] ) )
$content = $this -> normalizeContent( $data['choices'][0]['message']['content'] );
$content = trim( preg_replace( '/\s+/', ' ', $content ) );
$content = trim( $content, " \t\n\r\0\x0B\"'`." );
if ( $content === '' )
return '';
return mb_substr( $content, 0, self::TITLE_LIMIT );
}
private function normalizeContent( $content )
{
if ( is_string( $content ) )
return $content;
if ( !is_array( $content ) )
return '';
$parts = [];
foreach ( $content as $item )
{
if ( is_string( $item ) )
$parts[] = $item;
elseif ( is_array( $item ) && isset( $item['text'] ) && is_string( $item['text'] ) )
$parts[] = $item['text'];
elseif ( is_array( $item ) && isset( $item['text']['value'] ) && is_string( $item['text']['value'] ) )
$parts[] = $item['text']['value'];
}
return implode( ' ', $parts );
}
private function prepareTaskText( $task_text )
{
$task_text = html_entity_decode( (string)$task_text, ENT_QUOTES, 'UTF-8' );
$task_text = preg_replace( '/<script\b[^>]*>.*?<\/script>/is', ' ', $task_text );
$task_text = preg_replace( '/<style\b[^>]*>.*?<\/style>/is', ' ', $task_text );
$task_text = strip_tags( $task_text );
$task_text = preg_replace( '/\s+/', ' ', $task_text );
$task_text = trim( $task_text );
if ( $task_text === '' )
return '';
return mb_substr( $task_text, 0, self::CONTENT_LIMIT );
}
private function extractApiErrorMessage( $response_body )
{
$data = json_decode( (string)$response_body, true );
if ( is_array( $data ) && isset( $data['error']['message'] ) )
return (string)$data['error']['message'];
return mb_substr( trim( (string)$response_body ), 0, 300 );
}
private function error( $message )
{
return [
'status' => 'error',
'msg' => (string)$message
];
}
}