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
This commit is contained in:
190
autoload/Domain/Tasks/TaskTitleGenerator.php
Normal file
190
autoload/Domain/Tasks/TaskTitleGenerator.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?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
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -372,6 +372,51 @@ class Tasks
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function task_generate_title()
|
||||
{
|
||||
global $user, $settings;
|
||||
|
||||
$response = [ 'status' => 'error', 'msg' => 'Nie udało się wygenerować tytułu zadania.' ];
|
||||
|
||||
if ( !$user or !isset( $user['email'] ) or strtolower( trim( (string)$user['email'] ) ) !== 'biuro@project-pro.pl' )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Brak uprawnień do generowania tytułu.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$task_id = (int)\S::get( 'task_id' );
|
||||
if ( !$task_id )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Nieprawidłowe zadanie.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$task = \factory\Tasks::task_details( $task_id, (int)$user['id'] );
|
||||
if ( !is_array( $task ) or !isset( $task['id'] ) )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Nie znaleziono zadania.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$task_text = isset( $task['text'] ) ? trim( (string)$task['text'] ) : '';
|
||||
if ( $task_text === '' )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Zadanie nie ma treści do wygenerowania tytułu.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api_key = isset( $settings['openai_api_key'] ) ? trim( (string)$settings['openai_api_key'] ) : '';
|
||||
$model = isset( $settings['openai_task_title_model'] ) ? trim( (string)$settings['openai_task_title_model'] ) : 'gpt-5-nano';
|
||||
$generator = new \Domain\Tasks\TaskTitleGenerator();
|
||||
$result = $generator -> generate( $api_key, $model, $task_text );
|
||||
|
||||
if ( is_array( $result ) and isset( $result['status'] ) )
|
||||
$response = $result;
|
||||
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function task_change_text() {
|
||||
global $mdb;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user