|
8 | 8 | namespace Hametuha\Hamelp\Hooks; |
9 | 9 |
|
10 | 10 | use Hametuha\Hamelp\Pattern\Singleton; |
| 11 | +use Hametuha\Hamelp\Services\AiModelResolver; |
11 | 12 | use Hametuha\Hamelp\Services\FaqCatalogBuilder; |
12 | 13 |
|
13 | 14 | /** |
@@ -223,6 +224,52 @@ public function register_settings() { |
223 | 224 | ] |
224 | 225 | ); |
225 | 226 |
|
| 227 | + // Preferred AI model (auto-select by default; validated against the live registry). |
| 228 | + register_setting( |
| 229 | + self::OPTION_GROUP, |
| 230 | + AiModelResolver::OPTION_MODEL, |
| 231 | + [ |
| 232 | + 'type' => 'string', |
| 233 | + 'sanitize_callback' => [ $this, 'sanitize_model' ], |
| 234 | + 'default' => '', |
| 235 | + ] |
| 236 | + ); |
| 237 | + |
| 238 | + add_settings_field( |
| 239 | + AiModelResolver::OPTION_MODEL, |
| 240 | + __( 'AI Model', 'hamelp' ), |
| 241 | + [ $this, 'render_model_select' ], |
| 242 | + self::PAGE_SLUG, |
| 243 | + 'hamelp_ai_section', |
| 244 | + [ |
| 245 | + 'option_name' => AiModelResolver::OPTION_MODEL, |
| 246 | + ] |
| 247 | + ); |
| 248 | + |
| 249 | + // Sampling temperature (blank = omit, which is the default and safe for all models). |
| 250 | + register_setting( |
| 251 | + self::OPTION_GROUP, |
| 252 | + AiModelResolver::OPTION_TEMPERATURE, |
| 253 | + [ |
| 254 | + 'type' => 'string', |
| 255 | + 'sanitize_callback' => [ $this, 'sanitize_temperature' ], |
| 256 | + 'default' => '', |
| 257 | + ] |
| 258 | + ); |
| 259 | + |
| 260 | + add_settings_field( |
| 261 | + AiModelResolver::OPTION_TEMPERATURE, |
| 262 | + __( 'Temperature', 'hamelp' ), |
| 263 | + [ $this, 'render_text' ], |
| 264 | + self::PAGE_SLUG, |
| 265 | + 'hamelp_ai_section', |
| 266 | + [ |
| 267 | + 'option_name' => AiModelResolver::OPTION_TEMPERATURE, |
| 268 | + 'default' => '', |
| 269 | + 'description' => __( 'Sampling temperature between 0 and 2 (e.g. 0.3). Leave blank (the default) to omit it entirely — omitting is safe for every model, whereas some models (e.g. Claude Opus) return an error if a temperature is supplied.', 'hamelp' ), |
| 270 | + ] |
| 271 | + ); |
| 272 | + |
226 | 273 | // Citation reference label (customizable, translatable). |
227 | 274 | register_setting( |
228 | 275 | self::OPTION_GROUP, |
@@ -555,6 +602,91 @@ public function sanitize_mode( $value ) { |
555 | 602 | return in_array( $value, $allowed, true ) ? $value : 'conversation'; |
556 | 603 | } |
557 | 604 |
|
| 605 | + /** |
| 606 | + * Render the AI model select field. |
| 607 | + * |
| 608 | + * Choices are built from the live registry so only currently configured |
| 609 | + * providers/models appear. Warns when a previously saved model is no longer |
| 610 | + * available (e.g. its connector was disabled outside Hamelp). |
| 611 | + * |
| 612 | + * @param array $args Field arguments (option_name). |
| 613 | + */ |
| 614 | + public function render_model_select( array $args ) { |
| 615 | + if ( ! AiModelResolver::is_ai_available() ) { |
| 616 | + printf( |
| 617 | + '<p class="description">%s</p>', |
| 618 | + esc_html__( 'No AI provider is available. Connect one in Settings → Connectors, then reload this page.', 'hamelp' ) |
| 619 | + ); |
| 620 | + return; |
| 621 | + } |
| 622 | + |
| 623 | + $value = (string) get_option( $args['option_name'], '' ); |
| 624 | + $choices = [ '' => __( 'Auto (recommended)', 'hamelp' ) ] + AiModelResolver::get_available_models(); |
| 625 | + |
| 626 | + printf( '<select name="%1$s" id="%1$s">', esc_attr( $args['option_name'] ) ); |
| 627 | + foreach ( $choices as $key => $label ) { |
| 628 | + printf( |
| 629 | + '<option value="%s" %s>%s</option>', |
| 630 | + esc_attr( $key ), |
| 631 | + selected( $value, $key, false ), |
| 632 | + esc_html( $label ) |
| 633 | + ); |
| 634 | + } |
| 635 | + echo '</select>'; |
| 636 | + |
| 637 | + if ( AiModelResolver::is_stored_model_stale() ) { |
| 638 | + printf( |
| 639 | + '<p class="description" style="color:#b32d2e;">%s</p>', |
| 640 | + esc_html__( 'The previously selected model is no longer available (its connector may have been disabled). Auto-selection is being used until you choose an available model.', 'hamelp' ) |
| 641 | + ); |
| 642 | + } |
| 643 | + |
| 644 | + printf( |
| 645 | + '<p class="description">%s</p>', |
| 646 | + esc_html__( 'Pin the provider/model used for AI Overview. Only configured connectors are listed. If the chosen model becomes unavailable, Hamelp falls back to auto-selection instead of failing.', 'hamelp' ) |
| 647 | + ); |
| 648 | + } |
| 649 | + |
| 650 | + /** |
| 651 | + * Sanitize the AI model option. |
| 652 | + * |
| 653 | + * Accepts only the empty string (auto) or a currently available |
| 654 | + * `provider_id|model_id` value; anything else resets to auto. |
| 655 | + * |
| 656 | + * @param string $value Submitted value. |
| 657 | + * @return string A valid model key, or empty string for auto-select. |
| 658 | + */ |
| 659 | + public function sanitize_model( $value ) { |
| 660 | + $value = (string) $value; |
| 661 | + if ( '' === $value ) { |
| 662 | + return ''; |
| 663 | + } |
| 664 | + $available = AiModelResolver::get_available_models(); |
| 665 | + return isset( $available[ $value ] ) ? $value : ''; |
| 666 | + } |
| 667 | + |
| 668 | + /** |
| 669 | + * Sanitize the temperature option. |
| 670 | + * |
| 671 | + * Empty string is preserved (omit the parameter). A numeric value is clamped |
| 672 | + * to the 0–2 range. Any other input falls back to the default. |
| 673 | + * |
| 674 | + * @param string $value Submitted value. |
| 675 | + * @return string Sanitized temperature, or empty string to omit it. |
| 676 | + */ |
| 677 | + public function sanitize_temperature( $value ) { |
| 678 | + $value = trim( (string) $value ); |
| 679 | + if ( '' === $value ) { |
| 680 | + return ''; |
| 681 | + } |
| 682 | + if ( ! is_numeric( $value ) ) { |
| 683 | + return '0.3'; |
| 684 | + } |
| 685 | + $float = (float) $value; |
| 686 | + $float = max( 0.0, min( 2.0, $float ) ); |
| 687 | + return (string) $float; |
| 688 | + } |
| 689 | + |
558 | 690 | /** |
559 | 691 | * Render a checkbox field. |
560 | 692 | * |
|
0 commit comments