diff --git a/plugins/system/aicontentassistant/aicontentassistant.php b/plugins/system/aicontentassistant/aicontentassistant.php new file mode 100644 index 00000000000..f01becb9e01 --- /dev/null +++ b/plugins/system/aicontentassistant/aicontentassistant.php @@ -0,0 +1,852 @@ +app->isClient('administrator')) { + return; + } + + // Check if we are on an article edit page + if ($this->isArticleEditPage()) { + $this->addSimpleAIForm(); + } + } + + private function isArticleEditPage() + { + $input = $this->app->input; + $option = $input->get('option'); + $view = $input->get('view'); + $layout = $input->get('layout', 'default'); + + return $option === 'com_content' && $view === 'article' && $layout === 'edit'; + } + + private function addSimpleAIForm() + { + $html = ' + + + + '; + + $html .= ' + '; + + // Output the HTML directly + echo $html; + } + + public function onAjaxAicontentassistant() + { + // Force JSON content type + header('Content-Type: application/json'); + + try { + error_log('AI Plugin: AJAX handler called'); + + // Get the JSON input + $json = file_get_contents('php://input'); + error_log('AI Plugin: Raw input: ' . $json); + + $data = json_decode($json, true); + error_log('AI Plugin: Decoded data: ' . print_r($data, true)); + + if ($data['action'] === 'generate' && !empty($data['prompt'])) { + $prompt = $data['prompt']; + error_log('AI Plugin: Processing prompt: ' . $prompt); + + // SIMPLE: Just call the AI framework directly + $generatedContent = $this->callAI($prompt); + error_log('AI Plugin: Generated content length: ' . strlen($generatedContent)); + + $response = json_encode([ + 'success' => true, + 'content' => $generatedContent + ]); + + error_log('AI Plugin: Returning response: ' . substr($response, 0, 200) . '...'); + return $response; + } elseif ($data['action'] === 'introduction') { + $title = isset($data['title']) ? trim($data['title']) : ''; + $audience = isset($data['audience']) ? trim($data['audience']) : ''; + + if ($title === '') { + return json_encode([ + 'success' => false, + 'message' => 'Title is required.' + ]); + } + + $promptParts = []; + $promptParts[] = 'Write a single-paragraph introduction (about 60-120 words) for an article. Start with a hook.'; + $promptParts[] = "Article title: '" . $title . "'."; + if ($audience !== '') { $promptParts[] = 'Target audience: ' . $audience . '.'; } + $promptParts[] = 'Keep it concise, conversational, and avoid headings or bullet points.'; + $prompt = implode(' ', $promptParts); + + $generatedContent = $this->callAI($prompt); + + return json_encode([ + 'success' => true, + 'content' => $generatedContent + ]); + } elseif ($data['action'] === 'meta_description_auto') { + $title = isset($data['title']) ? trim($data['title']) : ''; + $content = isset($data['content']) ? trim($data['content']) : ''; + + if ($title === '' || $content === '') { + return json_encode([ + 'success' => false, + 'message' => 'Title and content are required.' + ]); + } + + // Build prompt using article excerpt + $excerpt = mb_substr($content, 0, 2000); + $prompt = "You are an SEO assistant. Generate a single SEO meta description for the following article. " + . "Constraints: maximum 300 characters, compelling, accurate, includes a natural call-to-action, no quotes, no line breaks, no emojis. " + . "Return ONLY the description text.\n\nTitle: " . $title . "\nArticle excerpt: " . $excerpt; + + $generatedContent = $this->callAI($prompt); + $generatedContent = trim(preg_replace('/\s+/', ' ', $generatedContent)); + $generatedContent = trim($generatedContent, "\"'“”‘’ "); + + // // Enforce character window + // if (mb_strlen($generatedContent) > 300) { + // $generatedContent = mb_substr($generatedContent, 0, 300); + // $generatedContent = preg_replace('/[^\p{L}\p{N}\)]*$/u', '', $generatedContent); + // } + + return json_encode([ + 'success' => true, + 'content' => $generatedContent + ]); + } elseif ($data['action'] === 'generate_image' && !empty($data['prompt'])) { + $prompt = trim($data['prompt']); + try { + $base64 = $this->callAI($prompt, 'image'); + + // Ensure we have a string + if (!is_string($base64) || $base64 === '') { + error_log('AI Plugin: Image generation empty (non-string or empty)'); + return json_encode([ + 'success' => false, + 'message' => 'Empty image response from provider.' + ]); + } + + // Strip prefix + if (str_starts_with($base64, 'data:')) { + $parts = explode(',', $base64, 2); + if (count($parts) === 2) { + $base64 = $parts[1]; + } + } + + // Basic base64 sanity check (length + charset) + if (!preg_match('/^[A-Za-z0-9+\/=]{100,}$/', $base64)) { + error_log('AI Plugin: Image base64 failed validation'); + return json_encode([ + 'success' => false, + 'message' => 'Image payload not recognized as base64.' + ]); + } + + error_log('AI Plugin: Image response (base64) length: ' . strlen($base64)); + return json_encode([ + 'success' => true, + 'image' => $base64 + ]); + } catch (Exception $eImg) { + error_log('AI Plugin: Image generation error: ' . $eImg->getMessage()); + return json_encode([ + 'success' => false, + 'message' => $eImg->getMessage() + ]); + } + } elseif ($data['action'] === 'generate_alt_text' && !empty($data['image_url'])) { + $imageUrl = trim($data['image_url']); + if ($imageUrl === '') { + return json_encode(['success' => false, 'message' => 'Image URL required']); + } + // Prompt for alt text + $prompt = 'You are an accessibility assistant. Your task is to generate concise, descriptive alt text for this image. Good alt text briefly describes key elements (people, objects, actions, setting, or text), avoids unnecessary detail, and skips quotes or phrases like- image of. Keep it under 125 characters and focus only on whats essential.'; + try { + $alt = $this->callAI($prompt, 'vision', ['image' => $imageUrl]); + $alt = trim(preg_replace('/\s+/', ' ', $alt)); + if (strlen($alt) > 130) { $alt = substr($alt, 0, 130); } + $alt = trim($alt, "\"'“”‘’ "); + return json_encode(['success' => true, 'alt' => $alt]); + } catch (Exception $ve) { + return json_encode(['success' => false, 'message' => $ve->getMessage()]); + } + } + + error_log('AI Plugin: Invalid request - no action or prompt'); + return json_encode([ + 'success' => false, + 'message' => 'No prompt provided' + ]); + + } catch (Exception $e) { + error_log('AI Plugin: Exception: ' . $e->getMessage()); + error_log('AI Plugin: Stack trace: ' . $e->getTraceAsString()); + + return json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); + } + } + + private function callAI($prompt, $type = 'text', $options = []) + { + // Load the AI framework files + $autoloadFile = JPATH_ROOT . '/libraries/gsoc25_ai_framework/vendor/autoload.php'; + + if (!file_exists($autoloadFile)) { + throw new Exception('AI Framework autoloader not found. Please run "composer install" in the framework directory.'); + } + + require_once $autoloadFile; + + // Get the selected provider from plugin settings + $provider = $this->params->get('default_provider', 'openai'); + error_log('AI Plugin: Using provider: ' . $provider); + + // Get provider-specific configuration + $config = $this->getProviderConfig($provider); + error_log('AI Plugin: Provider config prepared for: ' . $provider); + + try { + // Create AI instance with selected provider + $ai = \Joomla\AI\AIFactory::getAI($provider, $config); + + if ($type === 'image') { + $imgResponse = $ai->generateImage($prompt, ['response_format' => 'b64_json']); + if (is_object($imgResponse) && method_exists($imgResponse, 'getContent')) { + return $imgResponse->getContent(); + } + return (string) $imgResponse; + } + if ($type === 'vision') { + $imageUrl = $options['image'] ?? ''; + if ($imageUrl === '') { + throw new Exception('Request missing image.'); + } + + $visionResponse = $ai->vision($prompt, $imageUrl, $options); + if (is_object($visionResponse) && method_exists($visionResponse, 'getContent')) { + return $visionResponse->getContent(); + } + return (string) $visionResponse; + } + + // Default text mode + $response = $ai->chat($prompt); + return is_object($response) && method_exists($response, 'getContent') + ? $response->getContent() + : (string) $response; + } catch (Exception $e) { + error_log('AI Plugin: Provider error (' . $provider . '): ' . $e->getMessage()); + throw new Exception('AI Provider Error (' . ucfirst($provider) . '): ' . $e->getMessage()); + } + } + + private function getProviderConfig($provider) + { + switch ($provider) { + case 'openai': + $apiKey = $this->params->get('openai_api_key', ''); + if (empty($apiKey)) { + throw new Exception('OpenAI API key not configured. Please enter your API key in the plugin settings.'); + } + return ['api_key' => $apiKey]; + + case 'anthropic': + $apiKey = $this->params->get('anthropic_api_key', ''); + if (empty($apiKey)) { + throw new Exception('Anthropic API key not configured. Please enter your API key in the plugin settings.'); + } + return ['api_key' => $apiKey]; + + case 'ollama': + $baseUrl = $this->params->get('ollama_base_url', 'http://localhost:11434'); + if (empty($baseUrl)) { + throw new Exception('Ollama base URL not configured. Please enter the server URL in the plugin settings.'); + } + return ['base_url' => $baseUrl]; + + default: + throw new Exception('Unsupported AI provider: ' . $provider . '. Please select OpenAI, Anthropic, or Ollama.'); + } + } + +} \ No newline at end of file diff --git a/plugins/system/aicontentassistant/aicontentassistant.xml b/plugins/system/aicontentassistant/aicontentassistant.xml new file mode 100644 index 00000000000..85089728b3f --- /dev/null +++ b/plugins/system/aicontentassistant/aicontentassistant.xml @@ -0,0 +1,62 @@ + + + PLG_SYSTEM_AICONTENTASSISTANT + Joomla! Project + August 2025 + Copyright (C) 2025 Open Source Matters, Inc. All rights reserved. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 1.0.0 + PLG_SYSTEM_AICONTENTASSISTANT_XML_DESCRIPTION + + + aicontentassistant.php + language + + + + en-GB/plg_system_aicontentassistant.ini + en-GB/plg_system_aicontentassistant.sys.ini + + + + +
+ + + + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/plugins/system/aicontentassistant/language/en-GB/plg_system_aicontentassistant.ini b/plugins/system/aicontentassistant/language/en-GB/plg_system_aicontentassistant.ini new file mode 100644 index 00000000000..f304ab7a392 --- /dev/null +++ b/plugins/system/aicontentassistant/language/en-GB/plg_system_aicontentassistant.ini @@ -0,0 +1,68 @@ +; Joomla! Project +; Copyright (C) 2025 Open Source Matters, Inc. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_AICONTENTASSISTANT="System - AI Content Assistant" +PLG_SYSTEM_AICONTENTASSISTANT_XML_DESCRIPTION="AI-powered content generation assistant that integrates with Joomla's article editor. Provides AI assistance for creating article outlines, introductions, meta descriptions, and more using the GSoC 2025 AI Framework." + +; Basic Settings +PLG_SYSTEM_AICONTENTASSISTANT_FIELDSET_BASIC="Basic Settings" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_DEFAULT_PROVIDER="Default AI Provider" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_DEFAULT_PROVIDER_DESC="Select the default AI provider to use for content generation." + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_OPENAI_API_KEY="OpenAI API Key" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_OPENAI_API_KEY_DESC="Enter your OpenAI API key. Required if using OpenAI as provider." + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ANTHROPIC_API_KEY="Anthropic API Key" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ANTHROPIC_API_KEY_DESC="Enter your Anthropic API key. Required if using Anthropic as provider." + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_OLLAMA_BASE_URL="Ollama Base URL" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_OLLAMA_BASE_URL_DESC="Base URL for your Ollama installation. Default: http://localhost:11434" + +; Content Types +PLG_SYSTEM_AICONTENTASSISTANT_FIELDSET_CONTENT_TYPES="Content Generation Features" + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ENABLE_OUTLINE="Enable Article Outline Generation" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ENABLE_OUTLINE_DESC="Allow users to generate article outlines using AI." + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ENABLE_INTRO="Enable Introduction Generation" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ENABLE_INTRO_DESC="Allow users to generate article introductions using AI." + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ENABLE_META="Enable Meta Description Generation" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ENABLE_META_DESC="Allow users to generate SEO meta descriptions using AI." + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ENABLE_IMAGE="Enable Image Generation" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_ENABLE_IMAGE_DESC="Allow users to generate images using AI (requires OpenAI or compatible provider)." + +; Advanced Settings +PLG_SYSTEM_AICONTENTASSISTANT_FIELDSET_ADVANCED="Advanced Settings" + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_TEMPERATURE="Default Temperature" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_TEMPERATURE_DESC="Controls randomness in AI responses. Lower values = more focused, higher values = more creative. Range: 0.0 to 2.0" + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_MAX_TOKENS="Default Max Tokens" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_MAX_TOKENS_DESC="Maximum number of tokens for AI responses. Higher values allow longer content but use more API credits." + +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_DEBUG="Enable Debug Mode" +PLG_SYSTEM_AICONTENTASSISTANT_FIELD_DEBUG_DESC="Enable debug logging for troubleshooting. Only enable when needed." + +; Interface Labels +PLG_SYSTEM_AICONTENTASSISTANT_BUTTON_TEXT="AI Assistant" +PLG_SYSTEM_AICONTENTASSISTANT_MODAL_TITLE="AI Content Assistant" +PLG_SYSTEM_AICONTENTASSISTANT_GENERATE_BUTTON="Generate Content" +PLG_SYSTEM_AICONTENTASSISTANT_INSERT_BUTTON="Insert into Editor" +PLG_SYSTEM_AICONTENTASSISTANT_CLOSE_BUTTON="Close" + +; Content Types +PLG_SYSTEM_AICONTENTASSISTANT_TAB_OUTLINE="Article Outline" +PLG_SYSTEM_AICONTENTASSISTANT_TAB_INTRODUCTION="Introduction" +PLG_SYSTEM_AICONTENTASSISTANT_TAB_META="Meta Description" +PLG_SYSTEM_AICONTENTASSISTANT_TAB_IMAGE="Image Generation" + +; Messages +PLG_SYSTEM_AICONTENTASSISTANT_MSG_GENERATING="Generating content..." +PLG_SYSTEM_AICONTENTASSISTANT_MSG_SUCCESS="Content generated successfully!" +PLG_SYSTEM_AICONTENTASSISTANT_MSG_ERROR="Error generating content. Please try again." +PLG_SYSTEM_AICONTENTASSISTANT_MSG_NO_API_KEY="API key not configured for selected provider." +PLG_SYSTEM_AICONTENTASSISTANT_MSG_CONTENT_INSERTED="Content inserted into editor." diff --git a/plugins/system/aicontentassistant/language/en-GB/plg_system_aicontentassistant.sys.ini b/plugins/system/aicontentassistant/language/en-GB/plg_system_aicontentassistant.sys.ini new file mode 100644 index 00000000000..9cae5b76ab7 --- /dev/null +++ b/plugins/system/aicontentassistant/language/en-GB/plg_system_aicontentassistant.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; Copyright (C) 2025 Open Source Matters, Inc. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_AICONTENTASSISTANT="System - AI Content Assistant" +PLG_SYSTEM_AICONTENTASSISTANT_XML_DESCRIPTION="AI-powered content generation assistant that integrates with Joomla's article editor. Provides AI assistance for creating article outlines, introductions, meta descriptions, and more using the GSoC 2025 AI Framework."