99e4a83ed5
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
- AiGeneratorHelper: Claude Messages API and OpenAI Chat Completions with structured JSON output for social/short/chat/email_subject - AiController: AJAX endpoint with CSRF and ACL checks - config.xml: new AI fieldset (provider, API key, model, tone) - Content plugin: "Generate with AI" button in Share Content panel - Language strings for all AI config and UI elements Authored-by: Moko Consulting
197 lines
6.1 KiB
PHP
197 lines
6.1 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteCross
|
|
* @subpackage com_mokosuitecross
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteCross\Administrator\Helper;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
class AiGeneratorHelper
|
|
{
|
|
public static function generate(string $title, string $introtext, string $category, array $tags, array $config): array
|
|
{
|
|
$provider = $config['ai_provider'] ?? 'none';
|
|
$apiKey = $config['ai_api_key'] ?? '';
|
|
$model = $config['ai_model'] ?? '';
|
|
$tone = $config['ai_tone'] ?? 'professional';
|
|
|
|
if ($provider === 'none' || $apiKey === '') {
|
|
return ['success' => false, 'error' => 'AI provider not configured or API key missing.'];
|
|
}
|
|
|
|
$prompt = self::buildPrompt($title, $introtext, $category, $tags, $tone);
|
|
|
|
$response = match ($provider) {
|
|
'claude' => self::callClaude($prompt, $apiKey, $model ?: 'claude-haiku-4-5'),
|
|
'openai' => self::callOpenAI($prompt, $apiKey, $model ?: 'gpt-4o-mini'),
|
|
default => '',
|
|
};
|
|
|
|
if ($response === '') {
|
|
return ['success' => false, 'error' => 'AI provider returned an empty response.'];
|
|
}
|
|
|
|
$parsed = self::parseResponse($response);
|
|
|
|
if ($parsed === null) {
|
|
return ['success' => false, 'error' => 'Could not parse AI response as JSON.'];
|
|
}
|
|
|
|
return ['success' => true, 'data' => $parsed];
|
|
}
|
|
|
|
private static function callClaude(string $prompt, string $apiKey, string $model): string
|
|
{
|
|
$payload = json_encode([
|
|
'model' => $model,
|
|
'max_tokens' => 500,
|
|
'messages' => [
|
|
['role' => 'user', 'content' => $prompt],
|
|
],
|
|
]);
|
|
|
|
$ch = curl_init('https://api.anthropic.com/v1/messages');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'x-api-key: ' . $apiKey,
|
|
'anthropic-version: 2023-06-01',
|
|
],
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
|
|
if ($response === false) {
|
|
curl_close($ch);
|
|
return '';
|
|
}
|
|
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode < 200 || $httpCode >= 300) {
|
|
return '';
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
return $data['content'][0]['text'] ?? '';
|
|
}
|
|
|
|
private static function callOpenAI(string $prompt, string $apiKey, string $model): string
|
|
{
|
|
$payload = json_encode([
|
|
'model' => $model,
|
|
'max_tokens' => 500,
|
|
'messages' => [
|
|
['role' => 'user', 'content' => $prompt],
|
|
],
|
|
]);
|
|
|
|
$ch = curl_init('https://api.openai.com/v1/chat/completions');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $apiKey,
|
|
],
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
|
|
if ($response === false) {
|
|
curl_close($ch);
|
|
return '';
|
|
}
|
|
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode < 200 || $httpCode >= 300) {
|
|
return '';
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
return $data['choices'][0]['message']['content'] ?? '';
|
|
}
|
|
|
|
private static function buildPrompt(string $title, string $introtext, string $category, array $tags, string $tone): string
|
|
{
|
|
$tagList = !empty($tags) ? implode(', ', $tags) : 'none';
|
|
|
|
$toneGuide = match ($tone) {
|
|
'casual' => 'Use a relaxed, conversational tone.',
|
|
'friendly' => 'Use a warm, approachable tone with enthusiasm.',
|
|
default => 'Use a professional, polished tone.',
|
|
};
|
|
|
|
return <<<PROMPT
|
|
Generate cross-post captions for this article. {$toneGuide}
|
|
|
|
Article title: {$title}
|
|
Content summary: {$introtext}
|
|
Category: {$category}
|
|
Tags: {$tagList}
|
|
|
|
Return ONLY a JSON object with these keys (no markdown, no explanation):
|
|
{
|
|
"social": "Facebook/LinkedIn post (max 200 chars, include a call to action)",
|
|
"short": "Twitter/Bluesky post (max 270 chars, punchy, include 1-2 relevant hashtags)",
|
|
"chat": "Telegram/Discord message (max 300 chars, conversational)",
|
|
"email_subject": "Email subject line (max 60 chars, compelling, no clickbait)"
|
|
}
|
|
|
|
Rules:
|
|
- Do not include the article URL (it is added automatically)
|
|
- Do not wrap the JSON in markdown code fences
|
|
- Respect the character limits strictly
|
|
- Each caption should be unique, not just a reformatted version of the others
|
|
PROMPT;
|
|
}
|
|
|
|
private static function parseResponse(string $response): ?array
|
|
{
|
|
$response = trim($response);
|
|
|
|
if (preg_match('/\{[\s\S]*\}/', $response, $matches)) {
|
|
$response = $matches[0];
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
if (!\is_array($data)) {
|
|
return null;
|
|
}
|
|
|
|
$required = ['social', 'short', 'chat', 'email_subject'];
|
|
|
|
foreach ($required as $key) {
|
|
if (!isset($data[$key]) || !\is_string($data[$key])) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'social' => mb_substr($data['social'], 0, 500),
|
|
'short' => mb_substr($data['short'], 0, 280),
|
|
'chat' => mb_substr($data['chat'], 0, 500),
|
|
'email_subject' => mb_substr($data['email_subject'], 0, 120),
|
|
];
|
|
}
|
|
}
|