Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d4796b0
Add AI-powered chart creation and configuration assistant
vytisbulkevicius Mar 2, 2026
7e4ec2b
Fix PHPCS coding standards violations
vytisbulkevicius Mar 2, 2026
e822326
Fix PHPStan type errors and coding standards
vytisbulkevicius Mar 2, 2026
3916959
Fix PHPCS docblock spacing alignment
vytisbulkevicius Mar 2, 2026
a31a9a8
Fix AI features visibility and menu registration
vytisbulkevicius Mar 2, 2026
5bdad42
Fix AI feature UX issues in chart creation flow
vytisbulkevicius Mar 2, 2026
c173f76
Refine UX: center lock icon and force scroll to top
vytisbulkevicius Mar 2, 2026
7d7b35b
Fix scroll position and secure API key display
vytisbulkevicius Mar 2, 2026
4e52b89
Make API keys non-retrievable and fix scroll locking
vytisbulkevicius Mar 2, 2026
e901194
Fix scroll to top and restore masked API key display
vytisbulkevicius Mar 2, 2026
9c7161f
Fix API keys (remove button) and scroll (prevent checked radio autosc…
vytisbulkevicius Mar 2, 2026
20ff363
NUCLEAR FIX: Override scroll APIs to block WordPress auto-scroll
vytisbulkevicius Mar 2, 2026
85b6213
Fix layout shift - The REAL root cause (remove all scroll-blocking)
vytisbulkevicius Mar 2, 2026
9d67c67
Remove inline scroll script - IT was causing the modal jump
vytisbulkevicius Mar 2, 2026
94733a7
Improve AI vision prompt for accurate chart recognition and data extr…
vytisbulkevicius Mar 2, 2026
fdec9e7
Make AI image upload section border consistent when locked
vytisbulkevicius Mar 2, 2026
e44b1cd
Remove WP_Post type hint for compatibility with development branch
vytisbulkevicius Mar 4, 2026
17f93a9
Add capability check to renderChartPages for security
vytisbulkevicius Mar 4, 2026
6cf58f2
Trigger fresh build with cleared cache
vytisbulkevicius Mar 4, 2026
3eab534
Revert type hint to match pre-security PR state
vytisbulkevicius Mar 4, 2026
8005bfa
Add explicit comment for nonce creation without action parameter
vytisbulkevicius Mar 4, 2026
395d150
Fix chart creation nonce verification mismatch
vytisbulkevicius Mar 4, 2026
f7f9319
Merge branch 'development' into bugfix/fix-chart-creation-nonce
vytisbulkevicius Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,638 changes: 1,638 additions & 0 deletions classes/Visualizer/Module/AI.php

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions classes/Visualizer/Module/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,16 @@ public function registerAdminMenu() {
'admin.php?page=' . Visualizer_Plugin::NAME . '&vaction=addnew'
);

// Add AI Settings submenu
add_submenu_page(
Visualizer_Plugin::NAME,
__( 'AI Settings', 'visualizer' ),
__( 'AI Settings', 'visualizer' ),
'edit_posts',
'visualizer-ai-settings',
array( $this, 'renderAISettingsPage' )
);

$this->_supportPage = add_submenu_page(
Visualizer_Plugin::NAME,
__( 'Support', 'visualizer' ),
Expand Down Expand Up @@ -969,6 +979,19 @@ public function renderSupportPage() {
include_once VISUALIZER_ABSPATH . '/templates/support.php';
}

/**
* Renders AI Settings page.
*
* @since 3.12.0
*
* @access public
* @return void
*/
public function renderAISettingsPage() {
$render = new Visualizer_Render_Page_AISettings();
$render->render();
}

/**
* Renders visualizer library page.
*
Expand Down
40 changes: 38 additions & 2 deletions classes/Visualizer/Module/Chart.php
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ public function getCharts() {
*
* @access private
*
* @param WP_Post|null $chart The chart object.
* @param WP_Post $chart The chart object.
*
* @return array The array of chart data.
*/
Expand Down Expand Up @@ -638,6 +638,9 @@ public function renderChartPages() {

wp_register_style( 'visualizer-frame', VISUALIZER_ABSURL . 'css/frame.css', array( 'visualizer-chosen' ), Visualizer_Plugin::VERSION );
wp_register_script( 'visualizer-frame', VISUALIZER_ABSURL . 'js/frame.js', array( 'visualizer-chosen', 'jquery-ui-accordion', 'jquery-ui-tabs' ), Visualizer_Plugin::VERSION, true );
wp_register_script( 'visualizer-ai-config', VISUALIZER_ABSURL . 'js/ai-config.js', array( 'jquery', 'visualizer-frame' ), Visualizer_Plugin::VERSION, true );
wp_register_script( 'visualizer-ai-chart-from-image', VISUALIZER_ABSURL . 'js/ai-chart-from-image.js', array( 'jquery' ), Visualizer_Plugin::VERSION, true );
wp_register_script( 'visualizer-ai-chart-data-populate', VISUALIZER_ABSURL . 'js/ai-chart-data-populate.js', array( 'jquery' ), Visualizer_Plugin::VERSION, true );
wp_register_script( 'visualizer-customization', $this->get_user_customization_js(), array(), null, true );
wp_register_script(
'visualizer-render',
Expand Down Expand Up @@ -853,6 +856,8 @@ private function _handleDataAndSettingsPage() {
wp_enqueue_script( 'visualizer-preview' );
wp_enqueue_script( 'visualizer-chosen' );
wp_enqueue_script( 'visualizer-render' );
wp_enqueue_script( 'visualizer-ai-config' );
wp_enqueue_script( 'visualizer-ai-chart-data-populate' );

if ( Visualizer_Module::can_show_feature( 'simple-editor' ) ) {
wp_enqueue_script( 'visualizer-editor-simple' );
Expand Down Expand Up @@ -920,6 +925,16 @@ private function _handleDataAndSettingsPage() {
)
);

wp_localize_script(
'visualizer-ai-config',
'visualizerAI',
array(
'nonce' => wp_create_nonce( 'visualizer-ai-generate' ),
'chart_type' => $data['type'],
'ajaxurl' => admin_url( 'admin-ajax.php' ),
)
);

$render = new Visualizer_Render_Page_Data();
$render->chart = $this->_chart;
$render->type = $data['type'];
Expand Down Expand Up @@ -955,7 +970,7 @@ private function _handleDataAndSettingsPage() {
*/
private function _handleTypesPage() {
// process post request
if ( $_SERVER['REQUEST_METHOD'] === 'POST' && wp_verify_nonce( filter_input( INPUT_POST, 'nonce' ) ) ) {
if ( $_SERVER['REQUEST_METHOD'] === 'POST' && wp_verify_nonce( filter_input( INPUT_POST, 'nonce' ), 'visualizer-upload-data' ) ) {
$type = filter_input( INPUT_POST, 'type' );
$library = filter_input( INPUT_POST, 'chart-library' );
if ( Visualizer_Module_Admin::checkChartStatus( $type ) ) {
Expand Down Expand Up @@ -992,6 +1007,27 @@ private function _handleTypesPage() {
$render->chart = $this->_chart;
wp_enqueue_style( 'visualizer-frame' );
wp_enqueue_script( 'visualizer-frame' );
wp_enqueue_script( 'visualizer-ai-chart-from-image' );

// Localize script for AI image analysis
$has_openai = ! empty( get_option( 'visualizer_openai_api_key', '' ) );
$has_gemini = ! empty( get_option( 'visualizer_gemini_api_key', '' ) );
$has_claude = ! empty( get_option( 'visualizer_claude_api_key', '' ) );

wp_localize_script(
'visualizer-ai-chart-from-image',
'visualizerAI',
array(
'nonce_image' => wp_create_nonce( 'visualizer-ai-image' ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'has_openai' => $has_openai,
'has_gemini' => $has_gemini,
'has_claude' => $has_claude,
'chart_types' => Visualizer_Module_Admin::_getChartTypesLocalized( false, false, false, 'types' ),
'pro_url' => tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, 'aichartimage', 'chartfromimage' ),
)
);

wp_iframe( array( $render, 'render' ) );
}

Expand Down
2 changes: 1 addition & 1 deletion classes/Visualizer/Render/Layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ class="dashicons dashicons-lock"></span></h2>
add_query_arg(
array(
'action' => Visualizer_Module::is_pro() ? Visualizer_Pro::ACTION_FETCH_DATA : '',
'nonce' => wp_create_nonce( Visualizer_Pro::ACTION_FETCH_DATA ),
'nonce' => wp_create_nonce( 'visualizer-upload-data' ),
),
admin_url( 'admin-ajax.php' )
)
Expand Down
198 changes: 198 additions & 0 deletions classes/Visualizer/Render/Page/AISettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php
// +----------------------------------------------------------------------+
// | Copyright 2013 Madpixels (email : visualizer@madpixels.net) |
// +----------------------------------------------------------------------+
// | This program is free software; you can redistribute it and/or modify |
// | it under the terms of the GNU General Public License, version 2, as |
// | published by the Free Software Foundation. |
// | |
// | This program is distributed in the hope that it will be useful, |
// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
// | GNU General Public License for more details. |
// | |
// | You should have received a copy of the GNU General Public License |
// | along with this program; if not, write to the Free Software |
// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, |
// | MA 02110-1301 USA |
// +----------------------------------------------------------------------+

/**
* Renders AI settings page for API keys.
*
* @category Visualizer
* @package Render
* @subpackage Page
*
* @since 3.12.0
*/
class Visualizer_Render_Page_AISettings extends Visualizer_Render_Page {

/**
* Masks an API key to show only first and last few characters.
*
* @since 3.12.0
*
* @access private
* @param string $key The API key to mask.
* @return string The masked API key.
*/
private function _maskAPIKey( $key ) {
if ( empty( $key ) ) {
return '';
}

$length = strlen( $key );
if ( $length <= 12 ) {
// For short keys, show first 4 and mask the rest
return substr( $key, 0, 4 ) . str_repeat( '*', $length - 4 );
}

// For longer keys, show first 6 and last 4 characters
return substr( $key, 0, 6 ) . str_repeat( '*', $length - 10 ) . substr( $key, -4 );
}

/**
* Renders page content.
*
* @since 3.12.0
*
* @access protected
* @return void
*/
protected function _renderContent() {
echo '<div class="wrap">';
echo '<h1>' . esc_html__( 'Visualizer AI Settings', 'visualizer' ) . '</h1>';

// Check if PRO features are locked
$is_locked = ! Visualizer_Module_Admin::proFeaturesLocked();

if ( $is_locked ) {
// Show locked state with upgrade message
echo '<div style="position: relative; min-height: 400px;">';
echo '<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.95); z-index: 10; display: flex; align-items: center; justify-content: center;">';
echo '<div style="text-align: center; padding: 40px; max-width: 600px;">';
echo '<span class="dashicons dashicons-lock" style="font-size: 64px; color: #999; margin-bottom: 20px; display: block;"></span>';
echo '<h2 style="margin: 20px 0; color: #333;">' . esc_html__( 'AI Features - Premium Feature', 'visualizer' ) . '</h2>';
echo '<p style="margin: 20px 0; color: #666; font-size: 16px; line-height: 1.6;">' . esc_html__( 'AI-powered chart creation and configuration is available exclusively in Visualizer PRO. Upgrade now to unlock:', 'visualizer' ) . '</p>';
echo '<ul style="text-align: left; display: inline-block; margin: 20px 0; color: #666;">';
echo '<li style="margin: 10px 0;">✓ ' . esc_html__( 'AI Chart Configuration Assistant', 'visualizer' ) . '</li>';
echo '<li style="margin: 10px 0;">✓ ' . esc_html__( 'Create Charts from Images', 'visualizer' ) . '</li>';
echo '<li style="margin: 10px 0;">✓ ' . esc_html__( 'Natural Language Chart Customization', 'visualizer' ) . '</li>';
echo '<li style="margin: 10px 0;">✓ ' . esc_html__( 'Support for ChatGPT, Gemini & Claude', 'visualizer' ) . '</li>';
echo '</ul>';
echo '<a href="' . tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, 'aisettings', 'upgrade' ) . '" target="_blank" class="button button-primary" style="margin-top: 20px; padding: 10px 30px; height: auto; font-size: 16px;">';
echo esc_html__( 'Upgrade to PRO', 'visualizer' );
echo '</a>';
echo '</div>';
echo '</div>';
}

// Wrap the form in a div that will be overlaid if locked
echo '<div style="' . ( $is_locked ? 'opacity: 0.5; pointer-events: none;' : '' ) . '">';

// Check if form was submitted
if ( ! $is_locked && isset( $_POST['visualizer_ai_settings_nonce'] ) && wp_verify_nonce( $_POST['visualizer_ai_settings_nonce'], 'visualizer_ai_settings' ) ) {
$this->_saveSettings();
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved successfully.', 'visualizer' ) . '</p></div>';
}

// Get saved API keys
$openai_key = get_option( 'visualizer_openai_api_key', '' );
$gemini_key = get_option( 'visualizer_gemini_api_key', '' );
$claude_key = get_option( 'visualizer_claude_api_key', '' );

// Mask the keys for display
$openai_key_display = $this->_maskAPIKey( $openai_key );
$gemini_key_display = $this->_maskAPIKey( $gemini_key );
$claude_key_display = $this->_maskAPIKey( $claude_key );

echo '<form method="post" action="">';
wp_nonce_field( 'visualizer_ai_settings', 'visualizer_ai_settings_nonce' );

echo '<table class="form-table">';

// OpenAI API Key
echo '<tr>';
echo '<th scope="row"><label for="visualizer_openai_api_key">' . esc_html__( 'OpenAI API Key (ChatGPT)', 'visualizer' ) . '</label></th>';
echo '<td>';
echo '<input type="text" id="visualizer_openai_api_key" name="visualizer_openai_api_key" value="' . esc_attr( $openai_key_display ) . '" class="regular-text" placeholder="' . esc_attr__( 'Enter API key', 'visualizer' ) . '" autocomplete="off" />';
echo '<p class="description">' . esc_html__( 'Enter your OpenAI API key to enable ChatGPT integration.', 'visualizer' ) . ' <a href="https://platform.openai.com/api-keys" target="_blank">' . esc_html__( 'Get API Key', 'visualizer' ) . '</a></p>';
echo '</td>';
echo '</tr>';

// Gemini API Key
echo '<tr>';
echo '<th scope="row"><label for="visualizer_gemini_api_key">' . esc_html__( 'Google Gemini API Key', 'visualizer' ) . '</label></th>';
echo '<td>';
echo '<input type="text" id="visualizer_gemini_api_key" name="visualizer_gemini_api_key" value="' . esc_attr( $gemini_key_display ) . '" class="regular-text" placeholder="' . esc_attr__( 'Enter API key', 'visualizer' ) . '" autocomplete="off" />';
echo '<p class="description">' . esc_html__( 'Enter your Google Gemini API key.', 'visualizer' ) . ' <a href="https://makersuite.google.com/app/apikey" target="_blank">' . esc_html__( 'Get API Key', 'visualizer' ) . '</a></p>';
echo '</td>';
echo '</tr>';

// Claude API Key
echo '<tr>';
echo '<th scope="row"><label for="visualizer_claude_api_key">' . esc_html__( 'Anthropic Claude API Key', 'visualizer' ) . '</label></th>';
echo '<td>';
echo '<input type="text" id="visualizer_claude_api_key" name="visualizer_claude_api_key" value="' . esc_attr( $claude_key_display ) . '" class="regular-text" placeholder="' . esc_attr__( 'Enter API key', 'visualizer' ) . '" autocomplete="off" />';
echo '<p class="description">' . esc_html__( 'Enter your Anthropic Claude API key.', 'visualizer' ) . ' <a href="https://console.anthropic.com/account/keys" target="_blank">' . esc_html__( 'Get API Key', 'visualizer' ) . '</a></p>';
echo '</td>';
echo '</tr>';

echo '</table>';

echo '<p class="submit">';
echo '<input type="submit" name="submit" id="submit" class="button button-primary" value="' . esc_attr__( 'Save Settings', 'visualizer' ) . '">';
echo '</p>';

echo '</form>';

echo '</div>'; // End opacity wrapper

if ( $is_locked ) {
echo '</div>'; // End position relative wrapper
}

echo '</div>'; // End wrap
}

/**
* Saves AI settings.
*
* @since 3.12.0
*
* @access private
* @return void
*/
private function _saveSettings() {
// Get current keys
$current_openai = get_option( 'visualizer_openai_api_key', '' );
$current_gemini = get_option( 'visualizer_gemini_api_key', '' );
$current_claude = get_option( 'visualizer_claude_api_key', '' );

// Only update OpenAI key if a new value is provided and it's not the masked version
if ( isset( $_POST['visualizer_openai_api_key'] ) && ! empty( $_POST['visualizer_openai_api_key'] ) ) {
$new_key = sanitize_text_field( $_POST['visualizer_openai_api_key'] );
if ( $new_key !== $this->_maskAPIKey( $current_openai ) ) {
update_option( 'visualizer_openai_api_key', $new_key );
}
}

// Only update Gemini key if a new value is provided and it's not the masked version
if ( isset( $_POST['visualizer_gemini_api_key'] ) && ! empty( $_POST['visualizer_gemini_api_key'] ) ) {
$new_key = sanitize_text_field( $_POST['visualizer_gemini_api_key'] );
if ( $new_key !== $this->_maskAPIKey( $current_gemini ) ) {
update_option( 'visualizer_gemini_api_key', $new_key );
}
}

// Only update Claude key if a new value is provided and it's not the masked version
if ( isset( $_POST['visualizer_claude_api_key'] ) && ! empty( $_POST['visualizer_claude_api_key'] ) ) {
$new_key = sanitize_text_field( $_POST['visualizer_claude_api_key'] );
if ( $new_key !== $this->_maskAPIKey( $current_claude ) ) {
update_option( 'visualizer_claude_api_key', $new_key );
}
}
}

}
Loading
Loading