[...], 'meta' => [...] ] * @param array $history * @param string $target_element_id Optional. When the turn came from the * builder's per-element AI buttons, the id of * the selected widget. * @return array|\WP_Error { assistant_message, operations, usage } */ public static function edit( string $message, array $context = [], array $history = [], string $target_element_id = '' ) { $target = self::resolve_target( $context, $target_element_id ); return AIService::run( 'edit', $message, $context, $history, [], $target ); } /** * Look the target element up in the trusted context layout. * * The client sends only an **id**. Type and current settings are read back out * of the layout the request already carries, so the widget the prompt describes * is by construction the widget that is on the page — a client that sent its * own `type` could otherwise have the model briefed on a Donors Wall while the * id pointed at a headline, and the resulting `update_block` would write * donors-wall keys onto it. * * An id that matches nothing returns `[]`, which degrades to a normal unscoped * edit rather than a scope that matches no element and drops every operation. * * @param array $context * @param string $element_id * @return array{id?: string, type?: string, settings?: array} */ private static function resolve_target( array $context, string $element_id ): array { $element_id = trim( $element_id ); if ( '' === $element_id ) { return []; } $layout = isset( $context['layout'] ) && is_array( $context['layout'] ) ? $context['layout'] : []; foreach ( (array) ( $layout['columns'] ?? [] ) as $column ) { if ( ! is_array( $column ) ) { continue; } foreach ( (array) ( $column['elements'] ?? [] ) as $element ) { if ( ! is_array( $element ) ) { continue; } if ( (string) ( $element['id'] ?? '' ) !== $element_id ) { continue; } return [ 'id' => $element_id, 'type' => (string) ( $element['type'] ?? '' ), 'settings' => isset( $element['settings'] ) && is_array( $element['settings'] ) ? $element['settings'] : [], ]; } } return []; } }