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
191 lines
5.5 KiB
PHP
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
|
|
];
|
|
}
|
|
}
|