[], 'version' => PLUME_VERSION, ]; \wp_enqueue_script( 'plume-generator', PLUME_URL . 'assets/generator/index.js', array_merge( $asset['dependencies'], [ 'wp-element', 'wp-api-fetch', 'wp-i18n' ] ), $asset['version'], true ); \wp_localize_script( 'plume-generator', 'plumeData', [ 'nonce' => \wp_create_nonce( 'wp_rest' ), 'restUrl' => \esc_url_raw( \rest_url( 'plume/v1' ) ), 'currentPostId' => 0, 'isPaid' => TierManager::is_paid(), 'siteTitle' => \get_bloginfo( 'name' ), ] ); } /** * Register the /plume/v1/generate REST route. * * @since 1.0.0 * @return void */ public static function register_routes(): void { \register_rest_route( 'plume/v1', '/generate', [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ self::class, 'handle_generate' ], 'permission_callback' => function () { return \current_user_can( 'edit_posts' ); }, 'args' => [ 'title' => [ 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ], 'keywords' => [ 'type' => 'string', 'required' => false, 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ], 'tone' => [ 'type' => 'string', 'required' => false, 'sanitize_callback' => 'sanitize_text_field', 'default' => 'professional', ], 'length' => [ 'type' => 'string', 'required' => false, 'sanitize_callback' => 'sanitize_text_field', 'default' => 'medium', 'enum' => [ 'short', 'medium', 'long' ], ], ], ] ); } /** * Generate a draft post from the request parameters using the default AI provider. * * @since 1.0.0 * @param \WP_REST_Request $request Incoming REST request. * @return \WP_REST_Response 201 on success with post_id, edit_url, content, credits_used; 500 on error. */ public static function handle_generate( \WP_REST_Request $request ): \WP_REST_Response { $title = $request->get_param( 'title' ); $keywords = $request->get_param( 'keywords' ); $tone = $request->get_param( 'tone' ); $length = $request->get_param( 'length' ); $length_map = [ 'short' => '300–500', 'medium' => '600–900', 'long' => '1200–1800', ]; $word_count = $length_map[ $length ] ?? '600–900'; $prompt = "Write a complete blog post with the following specifications:\n\n" . "Title: {$title}\n" . ( $keywords ? "Keywords to include: {$keywords}\n" : '' ) . "Tone: {$tone}\n" . "Length: approximately {$word_count} words\n\n" . 'Write the post in Markdown. Use ## for main section headings and ### for sub-headings. ' . 'Return only the post body — no title, no preamble, no code fence around the post.'; try { $factory = new ProviderFactory( new ProviderSettings() ); $provider = $factory->make_default(); $voice = new \Plume\Voice\VoiceInjector(); $req = new \Plume\Providers\CompletionRequest( messages: [ [ 'role' => 'user', 'content' => $prompt, ], ], system: $voice->build_system_prompt( 'Post generation', \get_current_user_id() ), model: $provider->get_default_model(), temperature: 0.7, max_tokens: 2000, metadata: [ 'feature' => 'generator', 'post_title' => $title, ] ); $response = $provider->complete( $req ); // Usage logged by the provider layer: proxy for pro_managed, AbstractProvider::maybe_log() for pro_byok. $content = ( new \Plume\Content\ContentNormaliser() )->normalise( $response->content ); // Create a draft post. $post_id = \wp_insert_post( [ 'post_title' => \sanitize_text_field( $title ), 'post_content' => \wp_kses_post( $content ), 'post_status' => 'draft', 'post_author' => \get_current_user_id(), 'post_type' => 'post', ], true ); if ( \is_wp_error( $post_id ) ) { return new \WP_REST_Response( [ 'error' => $post_id->get_error_message() ], 500 ); } return new \WP_REST_Response( [ 'post_id' => $post_id, 'edit_url' => \get_edit_post_link( $post_id, 'raw' ), 'content' => $content, 'credits_used' => $response->credits_charged, ], 201 ); } catch ( \Throwable $e ) { if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log \error_log( '[Plume][Generator] ' . get_class( $e ) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() ); } return new \WP_REST_Response( [ 'error' => $e->getMessage() ], 500 ); } } }