From a240205ff1532672276dbef3edb836eb3ba520f4 Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Mon, 29 Jun 2026 08:19:55 +0530 Subject: [PATCH] feat: add OpenAI connection status indicator Surface whether the saved OpenAI API key is connected, mirroring the License field's status feedback. Previously saving a key gave no visual confirmation that it was valid and working. - Add Main::is_api_key_connected(), exposed as `isApiKeyConnected` in the dashboard options data. A key counts as connected when it is set and the last stored OpenAI error is not one that invalidates the key itself (transient errors such as rate limiting stay connected). - Show a "Connected" / "Not connected" indicator under the API Key field in Advanced settings, and hide the "Get an API key" link once connected. - Add unit tests for the connection helper. Part of Codeinwp/hyve#208 --- inc/Main.php | 64 ++++++++++++--- src/backend/parts/settings/Advanced.js | 49 +++++++++-- .../php/unit/tests/test-api-key-connected.php | 82 +++++++++++++++++++ 3 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 tests/php/unit/tests/test-api-key-connected.php diff --git a/inc/Main.php b/inc/Main.php index 55f2784b..fe0f9a23 100644 --- a/inc/Main.php +++ b/inc/Main.php @@ -158,21 +158,22 @@ function ( $data ) use ( $settings, $post_types_for_js ) { return array_merge( $data, [ - 'api' => $this->api->get_endpoint(), - 'rest_url' => rest_url( $this->api->get_endpoint() ), - 'postTypes' => $post_types_for_js, - 'hasAPIKey' => isset( $settings['api_key'] ) && ! empty( $settings['api_key'] ), - 'chunksLimit' => apply_filters( 'hyve_chunks_limit', 500 ), - 'isQdrantActive' => Qdrant_API::is_active(), - 'assets' => [ + 'api' => $this->api->get_endpoint(), + 'rest_url' => rest_url( $this->api->get_endpoint() ), + 'postTypes' => $post_types_for_js, + 'hasAPIKey' => isset( $settings['api_key'] ) && ! empty( $settings['api_key'] ), + 'isApiKeyConnected' => self::is_api_key_connected( $settings ), + 'chunksLimit' => apply_filters( 'hyve_chunks_limit', 500 ), + 'isQdrantActive' => Qdrant_API::is_active(), + 'assets' => [ 'images' => HYVE_LITE_URL . 'assets/images/', ], - 'stats' => $this->get_stats(), - 'docs' => 'https://docs.themeisle.com/article/2009-hyve-documentation', - 'qdrant_docs' => 'https://docs.themeisle.com/article/2066-integrate-hyve-with-qdrant', - 'pro' => 'https://themeisle.com/plugins/hyve/', - 'chart' => $this->get_chart_data(), - 'hasPro' => apply_filters( 'product_hyve_license_status', false ), + 'stats' => $this->get_stats(), + 'docs' => 'https://docs.themeisle.com/article/2009-hyve-documentation', + 'qdrant_docs' => 'https://docs.themeisle.com/article/2066-integrate-hyve-with-qdrant', + 'pro' => 'https://themeisle.com/plugins/hyve/', + 'chart' => $this->get_chart_data(), + 'hasPro' => apply_filters( 'product_hyve_license_status', false ), ] ); }, @@ -641,6 +642,43 @@ function ( $date ) { ]; } + /** + * Determine whether the saved OpenAI API key is connected. + * + * The key is validated against OpenAI whenever it is saved, and any + * key-related failure during use is stored in the error option. The key is + * considered connected when it is set and the last stored error (if any) is + * not one that invalidates the key itself. + * + * @param array $settings Plugin settings. + * + * @return bool + */ + public static function is_api_key_connected( $settings ) { + if ( empty( $settings['api_key'] ) ) { + return false; + } + + $last_error = get_option( OpenAI::ERROR_OPTION_KEY, false ); + + if ( ! is_array( $last_error ) || empty( $last_error['code'] ) ) { + return true; + } + + $key_error_codes = [ + 'invalid_api_key', + 'invalid_authentication', + 'account_deactivated', + 'billing_not_active', + 'organization_not_found', + 'organization_deactivated', + 'permission_denied', + 'insufficient_quota', + ]; + + return ! in_array( $last_error['code'], $key_error_codes, true ); + } + /** * Append services errors if they exists. * diff --git a/src/backend/parts/settings/Advanced.js b/src/backend/parts/settings/Advanced.js index ff6a99b5..65cabc53 100644 --- a/src/backend/parts/settings/Advanced.js +++ b/src/backend/parts/settings/Advanced.js @@ -9,6 +9,7 @@ import { BaseControl, Button, ExternalLink, + Icon, Panel, PanelRow, TextControl, @@ -20,6 +21,14 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { applyFilters } from '@wordpress/hooks'; +const getInitialApiStatus = () => { + if ( window.hyve?.isApiKeyConnected ) { + return 'connected'; + } + + return window.hyve?.hasAPIKey ? 'error' : 'none'; +}; + const Advanced = () => { const settings = useSelect( ( select ) => select( 'hyve' ).getSettings() ); @@ -29,6 +38,8 @@ const Advanced = () => { const [ isSaving, setIsSaving ] = useState( false ); + const [ apiStatus, setApiStatus ] = useState( getInitialApiStatus ); + const onSave = async () => { setIsSaving( true ); @@ -47,8 +58,10 @@ const Advanced = () => { if ( settings.api_key ) { setHasAPI( true ); + setApiStatus( 'connected' ); } else { setHasAPI( false ); + setApiStatus( 'none' ); } createNotice( 'success', __( 'Settings saved.', 'hyve-lite' ), { @@ -56,6 +69,10 @@ const Advanced = () => { isDismissible: true, } ); } catch ( error ) { + if ( settings.api_key ) { + setApiStatus( 'error' ); + } + createNotice( 'error', error, { type: 'snackbar', isDismissible: true, @@ -86,17 +103,37 @@ const Advanced = () => { featureComponent: 'api-key', featureValue: 'added', } ); + setApiStatus( 'editing' ); setSetting( 'api_key', newValue ); } } /> - - { __( 'Get an API key', 'hyve-lite' ) } - + { 'connected' === apiStatus && ( +

+ + { __( 'Connected', 'hyve-lite' ) } +

+ ) } + + { 'error' === apiStatus && ( +

+ + { __( + 'Not connected. Please check your API key.', + 'hyve-lite' + ) } +

+ ) } + + { 'connected' !== apiStatus && ( + + { __( 'Get an API key', 'hyve-lite' ) } + + ) }