'object', 'single' => true, 'show_in_rest' => false, 'revisions_enabled' => true, 'auth_callback' => static fn( $allowed, $key, $post_id ) => current_user_can( 'edit_post', $post_id ), ) ); register_post_meta( PostTypes::ARTICLE, self::HIDE_META, array( 'type' => 'boolean', 'single' => true, 'default' => false, 'show_in_rest' => true, 'auth_callback' => static fn( $allowed, $key, $post_id ) => current_user_can( 'edit_post', $post_id ), ) ); } /** * Stored summary. * * @param int $post_id Article. * @return array|null */ public static function get( $post_id ) { $data = get_post_meta( (int) $post_id, self::META, true ); return is_array( $data ) && ! empty( $data['points'] ) ? $data : null; } /** * Saves a summary (validated; extra fields kept as given). * * @param int $post_id Article. * @param array $data Summary. * @return array|\WP_Error Saved summary. */ public static function save( $post_id, array $data ) { $clean = self::validate( $data ); if ( is_wp_error( $clean ) ) { return $clean; } $data = array_merge( $data, $clean ); update_post_meta( (int) $post_id, self::META, $data ); return $data; } /** * Validates points, steps and minutes. * * @param array $data Raw summary (also an AI response). * @param int $min Fewest points. * @return array{points:string[],steps:string[],minutes:int}|\WP_Error */ public static function validate( array $data, $min = 1 ) { $line = static function ( $text, $max ) { $text = trim( preg_replace( '/\s+/u', ' ', wp_strip_all_tags( (string) $text ) ) ); $text = ltrim( $text, "-•*· \t" ); return mb_strlen( $text ) > $max ? rtrim( mb_substr( $text, 0, $max - 1 ) ) . '…' : $text; }; $points = array_values( array_filter( array_map( static fn( $p ) => $line( $p, self::MAX_POINT ), (array) ( $data['points'] ?? array() ) ) ) ); $steps = array_values( array_filter( array_map( static fn( $s ) => $line( $s, 120 ), (array) ( $data['steps'] ?? array() ) ) ) ); if ( count( $points ) < $min ) { return new \WP_Error( 'kdocs_invalid_summary', __( 'A summary needs at least one key point.', 'karma-docs' ), array( 'status' => 400 ) ); } return array( 'points' => array_slice( $points, 0, self::MAX_POINTS ), 'steps' => array_slice( $steps, 0, 8 ), 'minutes' => max( 0, min( 600, (int) ( $data['minutes'] ?? 0 ) ) ), ); } /** * Plain text of an article for summarizing: headings kept as "## …" lines, * long code blocks shortened. * * @param \WP_Post $post Article. * @return string */ public static function text( $post ) { $copy = clone $post; $copy->post_content = preg_replace_callback( '#(]*>)(.*?)()#is', static fn( $m ) => $m[1] . ( strlen( $m[2] ) > 600 ? substr( $m[2], 0, 600 ) . "\n…" : $m[2] ) . $m[3], (string) $post->post_content ); $out = array(); foreach ( Index::sections( $copy ) as $section ) { $out[] = ( $section['heading'] ? '## ' . $section['heading'] . "\n" : '' ) . $section['text']; } return trim( implode( "\n\n", $out ) ); } /** * Fingerprint of the article text a summary was made from. * * @param \WP_Post $post Article. * @return string */ public static function hash( $post ) { return md5( get_the_title( $post ) . "\n" . self::text( $post ) ); } /** * Word count of the article. * * @param \WP_Post $post Article. * @return int */ public static function words( $post ) { return count( Index::words( wp_strip_all_tags( strip_shortcodes( (string) $post->post_content ) ) ) ); } /** * Status: none, generated, edited, approved or outdated. * * @param \WP_Post|int $post Article. * @return string */ public static function status( $post ) { $post = get_post( $post ); $data = $post ? self::get( $post->ID ) : null; if ( ! $data ) { return 'none'; } if ( ! empty( $data['content_hash'] ) && self::hash( $post ) !== $data['content_hash'] ) { return 'outdated'; } return in_array( $data['status'] ?? '', array( 'edited', 'approved' ), true ) ? $data['status'] : 'generated'; } /** * The AI summary card for an article, when it should be shown. * * @param \WP_Post $post Article. * @return string HTML. */ public static function render( $post ) { $data = self::get( $post->ID ); if ( ! $data || ! Features::is_pro() || ! Settings::get( 'summary_show', true ) || get_post_meta( $post->ID, self::HIDE_META, true ) ) { return ''; } if ( Settings::get( 'summary_approved_only', false ) && 'approved' !== ( $data['status'] ?? '' ) ) { return ''; } if ( self::words( $post ) < (int) Settings::get( 'summary_min_words', 250 ) ) { return ''; } /** * Whether to show the AI summary on an article (Pro: per knowledge base setting). * * @param bool $show Show. * @param \WP_Post $post Article. */ if ( ! apply_filters( 'kdocs_show_summary', true, $post ) ) { return ''; } return self::card( $data, array( 'ai' => true, 'article' => $post->ID, 'approved' => 'approved' === ( $data['status'] ?? '' ), 'open' => (bool) Settings::get( 'summary_open', true ), ) ); } /** * Summary card markup (AI summary and the "Key points" block). * * @param array $data Summary: points, steps, minutes. * @param array $args ai (label + rating), article, approved, open, title. * @return string HTML. */ public static function card( array $data, array $args = array() ) { $args = wp_parse_args( $args, array( 'ai' => false, 'article' => 0, 'approved' => false, 'open' => true, 'title' => '', ) ); $title = $args['title'] ? $args['title'] : ( $args['ai'] ? __( 'Summary', 'karma-docs' ) : __( 'Key points', 'karma-docs' ) ); $note = $args['approved'] ? __( 'Generated from this article and reviewed by our team.', 'karma-docs' ) : __( 'Generated from this article by AI. Check the article for details.', 'karma-docs' ); ob_start(); ?>
> >

ID, '_kdocs_seo_description', true ) ); if ( '' !== $custom ) { return $custom; } if ( '' !== trim( (string) $post->post_excerpt ) ) { return wp_trim_words( wp_strip_all_tags( $post->post_excerpt ), 30, '…' ); } $data = self::get( $post->ID ); if ( $data && 'approved' === ( $data['status'] ?? '' ) && Features::is_pro() ) { $text = implode( ' ', $data['points'] ); return mb_strlen( $text ) > 158 ? rtrim( mb_substr( $text, 0, 157 ) ) . '…' : $text; } return wp_trim_words( wp_strip_all_tags( strip_shortcodes( excerpt_remove_blocks( (string) $post->post_content ) ) ), 30, '…' ); } }