diff --git a/classes/class-ai-api.php b/classes/class-ai-api.php new file mode 100644 index 0000000..c17e9f5 --- /dev/null +++ b/classes/class-ai-api.php @@ -0,0 +1,521 @@ +init(); + } + return self::$instance; + } + + /** + * Initialize the class. + */ + public function init() { + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + } + + /** + * Get connected model. + * The same function placed in /utils/is-ai-connected/ + * + * @return array|bool + */ + public function get_connected_model() { + $settings = get_option( 'mind_settings', array() ); + $ai_model = $settings['ai_model'] ?? ''; + $result = false; + + if ( $ai_model ) { + if ( 'gpt-4o' === $ai_model || 'gpt-4o-mini' === $ai_model ) { + if ( ! empty( $settings['openai_api_key'] ) ) { + $result = [ + 'model' => $ai_model, + 'key' => $settings['openai_api_key'], + ]; + } + } elseif ( ! empty( $settings['anthropic_api_key'] ) ) { + $result = [ + 'model' => 'claude-3-5-haiku' === $ai_model ? 'claude-3-5-haiku' : 'claude-3-5-sonnet', + 'key' => $settings['anthropic_api_key'], + ]; + } + } + + return $result; + } + + /** + * Send request to API. + * + * @param string $request request text. + * @param string $context context. + * + * @return mixed + */ + public function request( $request, $context ) { + // Set headers for streaming. + header( 'Content-Type: text/event-stream' ); + header( 'Cache-Control: no-cache' ); + header( 'Connection: keep-alive' ); + header( 'X-Accel-Buffering: no' ); + + ob_implicit_flush( true ); + ob_end_flush(); + + if ( ! $request ) { + $this->send_stream_error( 'no_request', __( 'Provide request to receive AI response.', 'mind' ) ); + exit; + } + + $connected_model = $this->get_connected_model(); + + if ( ! $connected_model ) { + $this->send_stream_error( 'no_model_connected', __( 'Select an AI model and provide API key in the plugin settings.', 'mind' ) ); + exit; + } + + $messages = $this->prepare_messages( $request, $context ); + + if ( 'gpt-4o' === $connected_model['model'] || 'gpt-4o-mini' === $connected_model['model'] ) { + $this->request_open_ai( $connected_model['model'], $connected_model['key'], $messages ); + } else { + $this->request_anthropic( $connected_model['model'], $connected_model['key'], $messages ); + } + + exit; + } + + /** + * Prepare messages for request. + * + * @param string $user_query user query. + * @param string $context context. + */ + public function prepare_messages( $user_query, $context ) { + $messages = []; + + $messages[] = [ + 'role' => 'system', + 'content' => Mind_Prompts::get_system_prompt( $user_query, $context ), + ]; + + // Optional blocks JSON context. + if ( $context ) { + $messages[] = [ + 'role' => 'user', + 'content' => '' . $context . '', + ]; + } + + // User Query. + $messages[] = [ + 'role' => 'user', + 'content' => '' . $user_query . '', + ]; + + return $messages; + } + + /** + * Convert OpenAI messages format to Anthropic format. + * + * @param array $openai_messages Array of messages in OpenAI format. + * @return array Messages in Anthropic format + */ + public function convert_to_anthropic_messages( $openai_messages ) { + $system = []; + $messages = []; + + foreach ( $openai_messages as $message ) { + if ( 'system' === $message['role'] ) { + $allow_cache = strlen( $message['content'] ) > 2100; + + // Convert system message. + $system[] = array_merge( + array( + 'type' => 'text', + 'text' => $message['content'], + ), + $allow_cache ? array( + 'cache_control' => [ 'type' => 'ephemeral' ], + ) : array() + ); + } else { + // Convert user/assistant messages. + $messages[] = [ + 'role' => 'assistant' === $message['role'] ? 'assistant' : 'user', + 'content' => $message['content'], + ]; + } + } + + return array( + 'system' => $system, + 'messages' => $messages, + ); + } + + /** + * Request Anthropic API. + * + * @param string $model model. + * @param string $key key. + * @param array $messages messages. + */ + public function request_anthropic( $model, $key, $messages ) { + $anthropic_messages = $this->convert_to_anthropic_messages( $messages ); + $anthropic_version = '2023-06-01'; + + if ( 'claude-3-5-haiku' === $model ) { + $model = 'claude-3-5-haiku-20241022'; + } else { + $model = 'claude-3-5-sonnet-20241022'; + } + + $body = [ + 'model' => $model, + 'max_tokens' => 8192, + 'system' => $anthropic_messages['system'], + 'messages' => $anthropic_messages['messages'], + 'stream' => true, + ]; + + /* phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init, WordPress.WP.AlternativeFunctions.curl_curl_setopt, WordPress.WP.AlternativeFunctions.curl_curl_exec, WordPress.WP.AlternativeFunctions.curl_curl_errno, WordPress.WP.AlternativeFunctions.curl_curl_error, WordPress.WP.AlternativeFunctions.curl_curl_close */ + + $ch = curl_init( 'https://api.anthropic.com/v1/messages' ); + curl_setopt( $ch, CURLOPT_POST, 1 ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + [ + 'Content-Type: application/json', + 'x-api-key: ' . $key, + 'anthropic-version: ' . $anthropic_version, + ] + ); + curl_setopt( $ch, CURLOPT_POSTFIELDS, wp_json_encode( $body ) ); + curl_setopt( + $ch, + CURLOPT_WRITEFUNCTION, + function ( $curl, $data ) { + // Response with error message. + if ( $data && strpos( $data, '{"type":"error","error":{' ) !== false ) { + $error_data = json_decode( $data, true ); + + if ( isset( $error_data['error']['message'] ) ) { + $this->send_stream_error( 'anthropic_error', $error_data['error']['message'] ); + } + + return strlen( $data ); + } + + $this->process_anthropic_stream_chunk( $data ); + + return strlen( $data ); + } + ); + + curl_exec( $ch ); + + if ( curl_errno( $ch ) ) { + $this->send_stream_error( 'curl_error', curl_error( $ch ) ); + } + + curl_close( $ch ); + } + + /** + * Request OpenAI API. + * + * @param string $model model. + * @param string $key key. + * @param array $messages messages. + */ + public function request_open_ai( $model, $key, $messages ) { + $body = [ + 'model' => $model, + 'stream' => true, + 'top_p' => 0.9, + 'temperature' => 0.7, + 'messages' => $messages, + ]; + + /* phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init, WordPress.WP.AlternativeFunctions.curl_curl_setopt, WordPress.WP.AlternativeFunctions.curl_curl_exec, WordPress.WP.AlternativeFunctions.curl_curl_errno, WordPress.WP.AlternativeFunctions.curl_curl_error, WordPress.WP.AlternativeFunctions.curl_curl_close */ + + $ch = curl_init( 'https://api.openai.com/v1/chat/completions' ); + curl_setopt( $ch, CURLOPT_POST, 1 ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $key, + ] + ); + curl_setopt( $ch, CURLOPT_POSTFIELDS, wp_json_encode( $body ) ); + curl_setopt( + $ch, + CURLOPT_WRITEFUNCTION, + function ( $curl, $data ) { + // Response with error message. + if ( $data && strpos( $data, "{\n \"error\": {\n \"message\":" ) !== false ) { + $error_data = json_decode( $data, true ); + + if ( isset( $error_data['error']['message'] ) ) { + $this->send_stream_error( 'openai_error', $error_data['error']['message'] ); + } + + return strlen( $data ); + } + + $this->process_openai_stream_chunk( $data ); + + return strlen( $data ); + } + ); + + curl_exec( $ch ); + + if ( curl_errno( $ch ) ) { + $this->send_stream_error( 'curl_error', curl_error( $ch ) ); + } + + curl_close( $ch ); + } + + /** + * Process streaming chunk from OpenAI + * + * @param string $chunk - chunk of data. + */ + private function process_openai_stream_chunk( $chunk ) { + $lines = explode( "\n", $chunk ); + + foreach ( $lines as $line ) { + if ( strlen( trim( $line ) ) === 0 ) { + continue; + } + + if ( strpos( $line, 'data: ' ) === 0 ) { + $json_data = trim( substr( $line, 6 ) ); + + if ( '[DONE]' === $json_data ) { + if ( ! empty( $this->buffer ) ) { + $this->send_buffered_chunk(); + } + $this->send_stream_chunk( [ 'done' => true ] ); + return; + } + + try { + $data = json_decode( $json_data, true ); + + if ( isset( $data['choices'][0]['delta']['content'] ) ) { + $content = $data['choices'][0]['delta']['content']; + + // Send immediately for JSON markers. + if ( strpos( $content, '```json' ) !== false || + strpos( $content, '```' ) !== false ) { + if ( ! empty( $this->buffer ) ) { + $this->send_buffered_chunk(); + } + $this->send_stream_chunk( [ 'content' => $content ] ); + $this->last_send_time = microtime( true ); + continue; + } + + $this->buffer .= $content; + $current_time = microtime( true ); + $time_since_last_send = $current_time - $this->last_send_time; + + if ( strlen( $this->buffer ) >= self::BUFFER_THRESHOLD || + $time_since_last_send >= self::MIN_SEND_INTERVAL || + strpos( $this->buffer, "\n" ) !== false ) { + $this->send_buffered_chunk(); + } + } + } catch ( Exception $e ) { + $this->send_stream_error( 'json_error', $e->getMessage() ); + } + } + } + } + + /** + * Process streaming chunk from Anthropic + * + * @param string $chunk - chunk of data. + */ + private function process_anthropic_stream_chunk( $chunk ) { + $lines = explode( "\n", $chunk ); + + foreach ( $lines as $line ) { + if ( strlen( trim( $line ) ) === 0 ) { + continue; + } + + // Remove "data: " prefix if exists. + if ( strpos( $line, 'data: ' ) === 0 ) { + $json_data = trim( substr( $line, 6 ) ); + } else { + $json_data = trim( $line ); + } + + // Skip empty events. + if ( '' === $json_data ) { + continue; + } + + try { + $data = json_decode( $json_data, true ); + + if ( isset( $data['type'] ) ) { + if ( 'content_block_delta' === $data['type'] && isset( $data['delta']['text'] ) ) { + $content = $data['delta']['text']; + + // Send immediately for JSON markers. + if ( + strpos( $content, '```json' ) !== false || + strpos( $content, '```' ) !== false + ) { + if ( ! empty( $this->buffer ) ) { + $this->send_buffered_chunk(); + } + + $this->send_stream_chunk( [ 'content' => $content ] ); + $this->last_send_time = microtime( true ); + } else { + $this->buffer .= $content; + $current_time = microtime( true ); + + $time_since_last_send = $current_time - $this->last_send_time; + + if ( + strlen( $this->buffer ) >= self::BUFFER_THRESHOLD || + $time_since_last_send >= self::MIN_SEND_INTERVAL || + strpos( $this->buffer, "\n" ) !== false + ) { + $this->send_buffered_chunk(); + } + } + } elseif ( 'message_stop' === $data['type'] ) { + if ( ! empty( $this->buffer ) ) { + $this->send_buffered_chunk(); + } + + $this->send_stream_chunk( [ 'done' => true ] ); + + return; + } + } + } catch ( Exception $e ) { + $this->send_stream_error( 'json_error', $e->getMessage() ); + } + } + } + + + /** + * Send buffered chunk + */ + private function send_buffered_chunk() { + if ( empty( $this->buffer ) ) { + return; + } + + $this->send_stream_chunk( + [ + 'content' => $this->buffer, + ] + ); + + $this->buffer = ''; + $this->last_send_time = microtime( true ); + } + + /** + * Send stream chunk + * + * @param array $data - data to send. + */ + private function send_stream_chunk( $data ) { + echo 'data: ' . wp_json_encode( $data ) . "\n\n"; + + if ( ob_get_level() > 0 ) { + ob_flush(); + } + + flush(); + } + + /** + * Send stream error + * + * @param string $code - error code. + * @param string $message - error message. + */ + private function send_stream_error( $code, $message ) { + $this->send_stream_chunk( + [ + 'error' => true, + 'code' => $code, + 'message' => $message, + ] + ); + } +} diff --git a/classes/class-assets.php b/classes/class-assets.php index 51a393f..c39a910 100644 --- a/classes/class-assets.php +++ b/classes/class-assets.php @@ -46,9 +46,8 @@ class Mind_Assets { * Enqueue editor assets */ public function enqueue_block_editor_assets() { - $settings = get_option( 'mind_settings', array() ); + $connected_model = Mind_AI_API::instance()->get_connected_model(); - $openai_key = $settings['openai_api_key'] ?? ''; $asset_data = $this->get_asset_file( 'build/editor' ); wp_enqueue_script( @@ -63,7 +62,7 @@ class Mind_Assets { 'mind-editor', 'mindData', [ - 'connected' => ! ! $openai_key, + 'connected' => ! ! $connected_model, 'settingsPageURL' => admin_url( 'admin.php?page=mind&sub_page=settings' ), ] ); diff --git a/classes/class-prompts.php b/classes/class-prompts.php index c2626d8..5c9e53a 100644 --- a/classes/class-prompts.php +++ b/classes/class-prompts.php @@ -31,13 +31,20 @@ You are Mind - an elite WordPress architect with years of experience in building - - Return a valid JSON array of block objects - - Each block must include: - - name (string): WordPress block identifier - - attributes (object): block-specific settings - - innerBlocks (array): nested block structures - - Ensure proper nesting for columns and groups - - Use placeholder URLs from https://placehold.co/ for images + - IMPORTANT: Response must start with ```json and end with ``` + - IMPORTANT: Always return blocks array, even for simple text (use core/paragraph) + - Response must be a valid JSON array of block objects + - Each block object must include: + - name (string): WordPress block identifier (e.g., "core/paragraph", "core/heading") + - attributes (object): All required block attributes + - innerBlocks (array): Can be empty [] but must be present + - For image blocks, use https://placehold.co/: + - Format: https://placehold.co/600x400 + - Sizes: Use common dimensions (600x400, 800x600, 1200x800) + - For complex layouts: + - Use core/columns with columnCount attribute + - Use core/group for section wrapping + - Maintain proper block hierarchy diff --git a/classes/class-rest.php b/classes/class-rest.php index 5920308..2737e64 100644 --- a/classes/class-rest.php +++ b/classes/class-rest.php @@ -13,34 +13,6 @@ if ( ! defined( 'ABSPATH' ) ) { * Class Mind_Rest */ class Mind_Rest extends WP_REST_Controller { - /** - * Buffer for streaming response. - * - * @var string - */ - private $buffer = ''; - - /** - * Last time the buffer was sent. - * - * @var int - */ - private $last_send_time = 0; - - /** - * Buffer threshold. - * - * @var int - */ - private const BUFFER_THRESHOLD = 150; - - /** - * Minimum send interval. - * - * @var float - */ - private const MIN_SEND_INTERVAL = 0.05; - /** * Namespace. * @@ -79,7 +51,7 @@ class Mind_Rest extends WP_REST_Controller { ] ); - // Request OpenAI API. + // Request AI API. register_rest_route( $namespace, '/request_ai/', @@ -105,7 +77,7 @@ class Mind_Rest extends WP_REST_Controller { } /** - * Get permissions for OpenAI api request. + * Get permissions for AI API request. * * @return bool */ @@ -136,257 +108,17 @@ class Mind_Rest extends WP_REST_Controller { } /** - * Prepare messages for request. - * - * @param string $user_query user query. - * @param string $context context. - */ - public function prepare_messages( $user_query, $context ) { - $messages = []; - - $messages[] = [ - 'role' => 'system', - 'content' => Mind_Prompts::get_system_prompt( $user_query, $context ), - ]; - - // Optional blocks JSON context. - if ( $context ) { - $messages[] = [ - 'role' => 'user', - 'content' => '' . $context . '', - ]; - } - - // User Query. - $messages[] = [ - 'role' => 'user', - 'content' => '' . $user_query . '', - ]; - - return $messages; - } - - /** - * Request OpenAI API. - * - * @param array $messages messages. - */ - public function request_open_ai( $messages ) { - $settings = get_option( 'mind_settings', array() ); - $openai_key = $settings['openai_api_key'] ?? ''; - - if ( ! $openai_key ) { - $this->send_stream_error( 'no_openai_key_found', __( 'Provide OpenAI key in the plugin settings.', 'mind' ) ); - exit; - } - - $body = [ - 'model' => 'gpt-4o', - 'stream' => true, - 'top_p' => 0.9, - 'temperature' => 0.7, - 'messages' => $messages, - ]; - - /* phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_init, WordPress.WP.AlternativeFunctions.curl_curl_setopt, WordPress.WP.AlternativeFunctions.curl_curl_exec, WordPress.WP.AlternativeFunctions.curl_curl_errno, WordPress.WP.AlternativeFunctions.curl_curl_error, WordPress.WP.AlternativeFunctions.curl_curl_close */ - - $ch = curl_init( 'https://api.openai.com/v1/chat/completions' ); - curl_setopt( $ch, CURLOPT_POST, 1 ); - curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); - curl_setopt( - $ch, - CURLOPT_HTTPHEADER, - [ - 'Content-Type: application/json', - 'Authorization: Bearer ' . $openai_key, - ] - ); - curl_setopt( $ch, CURLOPT_POSTFIELDS, wp_json_encode( $body ) ); - curl_setopt( - $ch, - CURLOPT_WRITEFUNCTION, - function ( $curl, $data ) { - // Response with error message. - if ( $data && strpos( $data, "{\n \"error\": {\n \"message\":" ) !== false ) { - $error_data = json_decode( $data, true ); - - if ( isset( $error_data['error']['message'] ) ) { - $this->send_stream_error( 'openai_error', $error_data['error']['message'] ); - } - - return strlen( $data ); - } - - $this->process_stream_chunk( $data ); - - return strlen( $data ); - } - ); - - curl_exec( $ch ); - - if ( curl_errno( $ch ) ) { - $this->send_stream_error( 'curl_error', curl_error( $ch ) ); - } - - curl_close( $ch ); - } - - /** - * Send request to OpenAI. + * Send request to AI API. * * @param WP_REST_Request $req request object. * * @return mixed */ public function request_ai( WP_REST_Request $req ) { - // Set headers for streaming. - header( 'Content-Type: text/event-stream' ); - header( 'Cache-Control: no-cache' ); - header( 'Connection: keep-alive' ); - header( 'X-Accel-Buffering: no' ); - - ob_implicit_flush( true ); - ob_end_flush(); - $request = $req->get_param( 'request' ) ?? ''; $context = $req->get_param( 'context' ) ?? ''; - if ( ! $request ) { - $this->send_stream_error( 'no_request', __( 'Provide request to receive AI response.', 'mind' ) ); - exit; - } - - $messages = $this->prepare_messages( $request, $context ); - - $this->request_open_ai( $messages ); - - exit; - } - - /** - * Build base string - * - * @param string $base_uri - url. - * @param string $method - method. - * @param array $params - params. - * - * @return string - */ - private function build_base_string( $base_uri, $method, $params ) { - $r = []; - ksort( $params ); - foreach ( $params as $key => $value ) { - $r[] = "$key=" . rawurlencode( $value ); - } - return $method . '&' . rawurlencode( $base_uri ) . '&' . rawurlencode( implode( '&', $r ) ); - } - - /** - * Process streaming chunk from OpenAI - * - * @param string $chunk - chunk of data. - */ - private function process_stream_chunk( $chunk ) { - $lines = explode( "\n", $chunk ); - - foreach ( $lines as $line ) { - if ( strlen( trim( $line ) ) === 0 ) { - continue; - } - - if ( strpos( $line, 'data: ' ) === 0 ) { - $json_data = trim( substr( $line, 6 ) ); - - if ( '[DONE]' === $json_data ) { - if ( ! empty( $this->buffer ) ) { - $this->send_buffered_chunk(); - } - $this->send_stream_chunk( [ 'done' => true ] ); - return; - } - - try { - $data = json_decode( $json_data, true ); - - if ( isset( $data['choices'][0]['delta']['content'] ) ) { - $content = $data['choices'][0]['delta']['content']; - - // Send immediately for JSON markers. - if ( strpos( $content, '```json' ) !== false || - strpos( $content, '```' ) !== false ) { - if ( ! empty( $this->buffer ) ) { - $this->send_buffered_chunk(); - } - $this->send_stream_chunk( [ 'content' => $content ] ); - $this->last_send_time = microtime( true ); - continue; - } - - $this->buffer .= $content; - $current_time = microtime( true ); - $time_since_last_send = $current_time - $this->last_send_time; - - if ( strlen( $this->buffer ) >= self::BUFFER_THRESHOLD || - $time_since_last_send >= self::MIN_SEND_INTERVAL || - strpos( $this->buffer, "\n" ) !== false ) { - $this->send_buffered_chunk(); - } - } - } catch ( Exception $e ) { - $this->send_stream_error( 'json_error', $e->getMessage() ); - } - } - } - } - - /** - * Send buffered chunk - */ - private function send_buffered_chunk() { - if ( empty( $this->buffer ) ) { - return; - } - - $this->send_stream_chunk( - [ - 'content' => $this->buffer, - ] - ); - - $this->buffer = ''; - $this->last_send_time = microtime( true ); - } - - /** - * Send stream chunk - * - * @param array $data - data to send. - */ - private function send_stream_chunk( $data ) { - echo 'data: ' . wp_json_encode( $data ) . "\n\n"; - - if ( ob_get_level() > 0 ) { - ob_flush(); - } - - flush(); - } - - /** - * Send stream error - * - * @param string $code - error code. - * @param string $message - error message. - */ - private function send_stream_error( $code, $message ) { - $this->send_stream_chunk( - [ - 'error' => true, - 'code' => $code, - 'message' => $message, - ] - ); + Mind_AI_API::instance()->request( $request, $context ); } /** diff --git a/mind.php b/mind.php index dbd5c12..29c75ec 100644 --- a/mind.php +++ b/mind.php @@ -1,8 +1,8 @@ plugin_path . 'classes/class-prompts.php'; + require_once $this->plugin_path . 'classes/class-ai-api.php'; require_once $this->plugin_path . 'classes/class-admin.php'; require_once $this->plugin_path . 'classes/class-assets.php'; require_once $this->plugin_path . 'classes/class-rest.php'; diff --git a/package.json b/package.json index 2fe1939..448dbb9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mind", "version": "0.2.0", - "description": "Mind - Content Assistant Plugin based on OpenAI", + "description": "Mind - AI Page Builder based on Anthropic and OpenAI. Build, design, improve, rewrite your page sections and blocks.", "author": "Mind Team", "license": "GPL-2.0-or-later", "files": [ diff --git a/readme.txt b/readme.txt index 853e34d..de400f7 100644 --- a/readme.txt +++ b/readme.txt @@ -1,6 +1,6 @@ -=== Mind - AI Content Assistant === +=== Mind - AI Page Builder === Contributors: nko -Tags: ai, openai, gpt, copywriting, assistant +Tags: ai, gpt, ai page builder, ai editor, copilot Requires at least: 6.2 Tested up to: 6.7 Requires PHP: 7.2 @@ -8,11 +8,11 @@ Stable tag: 0.2.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html -AI content assistant and enhancer for WordPress page builder. +AI page builder for WordPress which let's you build sections, redesign existing blocks, etc... == Description == -Mind is a WordPress plugin designed to assist content editors in writing and improving posts. Powered by the OpenAI API, Mind offers a range of features to enhance the content creation process. +Mind is a WordPress plugin that transforms your page building experience. Powered by AI technology, it helps you create and modify entire page sections, layouts, and content directly in the WordPress editor. With support for both Anthropic and OpenAI AI models, Mind seamlessly integrates with the WordPress block editor to enhance your page building workflow. === 🚀 Community-Driven Development === diff --git a/src/admin/page-settings/index.js b/src/admin/page-settings/index.js index ebbef8f..41d582d 100644 --- a/src/admin/page-settings/index.js +++ b/src/admin/page-settings/index.js @@ -21,12 +21,38 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import isValidOpenAIApiKey from '../../utils/is-valid-openai-api-key'; +import isValidAnthropicApiKey from '../../utils/is-valid-anthropic-api-key'; import { ReactComponent as LoadingIcon } from '../../icons/loading.svg'; +const models = [ + { + title: __('Claude 3.5 Sonnet', 'mind'), + name: 'claude-3-5-sonnet', + description: __('Best quality and recommended', 'mind'), + }, + { + title: __('Claude 3.5 Haiku', 'mind'), + name: 'claude-3-5-haiku', + description: __('Fast and accurate', 'mind'), + }, + { + title: __('GPT-4o', 'mind'), + name: 'gpt-4o', + description: __('Quick and reliable', 'mind'), + }, + { + title: __('GPT-4o mini', 'mind'), + name: 'gpt-4o-mini', + description: __('Basic and fastest', 'mind'), + }, +]; + export default function PageSettings() { const [pendingSettings, setPendingSettings] = useState({}); const [settingsChanged, setSettingsChanged] = useState(false); - const [isInvalidAPIKey, setIsInvalidAPIKey] = useState(false); + const [isInvalidAnthropicAPIKey, setIsInvalidAnthropicAPIKey] = + useState(false); + const [isInvalidOpenAIAPIKey, setIsInvalidOpenAIAPIKey] = useState(false); const { updateSettings } = useDispatch('mind/settings'); @@ -54,12 +80,118 @@ export default function PageSettings() { <>
-
+
+ {models.map((model) => ( + + ))} +
+
+ {pendingSettings.ai_model?.includes('claude') && ( +
+
+ +
+
+ { + e.preventDefault(); + setPendingSettings({ + ...pendingSettings, + anthropic_api_key: e.target.value, + }); + }} + /> + {isInvalidAnthropicAPIKey && ( +
+ {__('Please enter a valid API key', 'mind')} +
+ )} +
{__( - 'This setting is required, since our plugin works with OpenAI.', + 'This setting is required to use Anthropic models.', + 'mind' + )}{' '} + + {__('Create API key', 'mind')} + +
+
+ )} + + {pendingSettings.ai_model?.includes('gpt') && ( +
+
+ +
+
+ { + e.preventDefault(); + setPendingSettings({ + ...pendingSettings, + openai_api_key: e.target.value, + }); + }} + /> + {isInvalidOpenAIAPIKey && ( +
+ {__('Please enter a valid API key', 'mind')} +
+ )} +
+
+ {__( + 'This setting is required to use OpenAI models.', 'mind' )}{' '}
-
- { - e.preventDefault(); - setPendingSettings({ - ...pendingSettings, - openai_api_key: e.target.value, - }); - }} - /> - {isInvalidAPIKey && ( -
- {__('Please enter a valid API key', 'mind')} -
- )} -
- + )} + {error &&
{error}
}
)} diff --git a/src/editor/popup/components/not-connected-screen/index.js b/src/editor/popup/components/not-connected-screen/index.js index 1a7deea..73f0c11 100644 --- a/src/editor/popup/components/not-connected-screen/index.js +++ b/src/editor/popup/components/not-connected-screen/index.js @@ -27,12 +27,12 @@ export default function NotConnectedScreen() {

- {__('OpenAI Key', 'mind')} + {__('AI API Key', 'mind')}

{__( - 'In order to use Mind, you will need to provide your OpenAI API key. Please insert your API key in the plugin settings to get started.', + 'In order to use Mind, you will need to provide your Anthropic or OpenAI API key. Please insert your API key in the plugin settings to get started.', 'mind' )}

@@ -41,6 +41,8 @@ export default function NotConnectedScreen() {
{__('Go to Settings', 'mind')} diff --git a/src/editor/popup/components/not-connected-screen/style.scss b/src/editor/popup/components/not-connected-screen/style.scss index ac5faba..85f57d2 100644 --- a/src/editor/popup/components/not-connected-screen/style.scss +++ b/src/editor/popup/components/not-connected-screen/style.scss @@ -1,9 +1,5 @@ @import "../../../../mixins/text-gradient"; -.mind-popup-not-connected { - width: 440px; -} - .mind-popup-connected-screen { display: flex; flex-direction: column; diff --git a/src/utils/is-ai-connected/index.js b/src/utils/is-ai-connected/index.js new file mode 100644 index 0000000..829ffe7 --- /dev/null +++ b/src/utils/is-ai-connected/index.js @@ -0,0 +1,22 @@ +/** + * Check if AI is connected + * The same function is placed in /classes/class-ai-api.php + * + * @param {Object} settings Settings object + * + * @return {boolean} is connected + */ +export default function isAIConnected(settings) { + const model = settings.ai_model || ''; + let result = false; + + if (model) { + if ('gpt-4o' === model || 'gpt-4o-mini' === model) { + result = !!settings?.openai_api_key; + } else if (settings?.anthropic_api_key) { + result = !!settings?.anthropic_api_key; + } + } + + return result; +} diff --git a/src/utils/is-valid-anthropic-api-key/index.js b/src/utils/is-valid-anthropic-api-key/index.js new file mode 100644 index 0000000..8f9f85b --- /dev/null +++ b/src/utils/is-valid-anthropic-api-key/index.js @@ -0,0 +1,4 @@ +export default function isValidAnthropicApiKey(apiKey) { + const regex = /^sk-ant-[a-zA-Z0-9]/; + return regex.test(apiKey); +}