Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions gc-openai/gcoai-stream-loading-text-animation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php
/**
* Gravity Connect // OpenAI // Stream Loading Text Animation
*
* Adds a customizable shimmer animation and rotating spinner icon to the Stream field's

Check failure on line 5 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
* loading placeholders. Replaces the static "Loading..." text with animated text and/or

Check failure on line 6 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
* a rotating spinner icon.
*/
class GCOAI_Loading_Animation {

private $args;

public function __construct( $args = array() ) {
$this->args = wp_parse_args( $args, array(
'text' => 'Thinking...',
'base_color' => '#000',
'shimmer_color' => '#fff',
'shimmer_duration' => '2.2s',
'show_shimmer' => true,
'show_spinner' => false,
'spinner_size' => '24',
'form_id' => null,
'field_id' => null,
) );

add_filter( 'gform_gcoai_field_loading_text', array( $this, 'filter_loading_text' ), 10, 3 );
add_action( 'gform_register_init_scripts', array( $this, 'register_init_script' ), 10, 2 );
}

public function register_init_script( $form, $is_ajax ) {
if ( empty( $form['id'] ) ) {
return;
}

Check failure on line 34 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
// If form_id is specified, only run scripts on those forms
if ( $this->args['form_id'] !== null ) {
$form_ids = is_array( $this->args['form_id'] ) ? $this->args['form_id'] : array( $this->args['form_id'] );
if ( ! in_array( $form['id'], $form_ids ) ) {
return;
}
}

$markup = $this->get_shimmer_markup();
$css = $this->get_styles_css();

Check failure on line 45 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
?>
<script type="text/javascript">
(function($) {
var shimmerMarkup = <?php echo wp_json_encode( $markup ); ?>;
var shimmerStyles = <?php echo wp_json_encode( $css ); ?>;

function addStylesToPage() {
if ( ! $('style.gw-gcoai-shimmer-style').length ) {
$('<style>')
.addClass('gw-gcoai-shimmer-style')
.text(shimmerStyles)
.appendTo('head');
}
}

function applyShimmerToPlaceholders($container) {
var $searchContext = $container && $container.length ? $container : $(document);
$searchContext.find('.gcoai-output .gcoai-placeholder').html(shimmerMarkup);
}

if ( window.gform && typeof window.gform.addFilter === 'function' ) {
window.gform.addFilter('gcoai_stream_loading_placeholder', function(current, instance) {
return shimmerMarkup;
});
}

$(function() {
addStylesToPage();
applyShimmerToPlaceholders();
});

// Re-apply after Generate/Regenerate clicks
$(document).on('click', '.gcoai-trigger, .gcoai-regenerate', function() {
setTimeout(function() {
applyShimmerToPlaceholders();
}, 50);
});

// Re-apply after AJAX completes
$(document).ajaxComplete(function() {
applyShimmerToPlaceholders();
});
})(jQuery);
</script>
<?php
}

public function get_shimmer_markup() {
$spinner = '';

Check failure on line 95 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
if ( $this->args['show_spinner'] ) {
$spinner = sprintf(
'<svg class="shimmer-spinner" xmlns="http://www.w3.org/2000/svg" width="%s" height="%s" stroke="%s" viewBox="0 0 24 24">
<g class="spinner-rotate">
<circle cx="12" cy="12" r="9.5" fill="none" stroke-width="1.5"/>
</g>
</svg>',
esc_attr( $this->args['spinner_size'] ),
esc_attr( $this->args['spinner_size'] ),
esc_attr( $this->args['base_color'] )
);
}

$text_class = $this->args['show_shimmer'] ? 'shimmer' : 'shimmer-text';

Check failure on line 110 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
return sprintf(
'<span class="shimmer-wrapper">%s<span class="%s">%s</span></span>',
$spinner,
$text_class,
esc_html( $this->args['text'] )
);
}

public function filter_loading_text( $placeholder, $field, $form = null ) {
if ( ! class_exists( '\\GC_OpenAI\\Fields\\Stream' ) || ! $field instanceof \GC_OpenAI\Fields\Stream ) {
return $placeholder;
}

Check failure on line 123 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
// If form_id is specified, only apply to those forms
if ( $this->args['form_id'] !== null ) {
$form_ids = is_array( $this->args['form_id'] ) ? $this->args['form_id'] : array( $this->args['form_id'] );
if ( $form && ! in_array( rgar( $form, 'id' ), $form_ids ) ) {
return $placeholder;
}
}

Check failure on line 131 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
// If field_id is specified, only apply to those fields
if ( $this->args['field_id'] !== null ) {
$field_ids = is_array( $this->args['field_id'] ) ? $this->args['field_id'] : array( $this->args['field_id'] );
if ( ! in_array( rgar( $field, 'id' ), $field_ids ) ) {
return $placeholder;
}
}

Check failure on line 139 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
return $this->get_shimmer_markup();
}
Comment on lines 135 to 149
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

field_id targeting is broken due to use of rgar on a field object

In filter_loading_text(), the form scoping uses rgar( $form, 'id' ), which is fine because $form is an array. For fields, however, you already check $field instanceof \GC_OpenAI\Fields\Stream, so $field is an object. Passing it to rgar( $field, 'id' ) will always produce an empty string, meaning the field_id filter never matches and the shimmer is never applied when field_id is configured.

You can fix this by reading $field->id directly and normalizing types for comparison:

-		// If field_id is specified, only apply to those fields
-		if ( $this->args['field_id'] !== null ) {
-			$field_ids = is_array( $this->args['field_id'] ) ? $this->args['field_id'] : array( $this->args['field_id'] );
-			if ( ! in_array( rgar( $field, 'id' ), $field_ids ) ) {
-				return $placeholder;
-			}
-		}
+		// If field_id is specified, only apply to those fields
+		if ( $this->args['field_id'] !== null && isset( $field->id ) ) {
+			$field_ids = is_array( $this->args['field_id'] ) ? $this->args['field_id'] : array( $this->args['field_id'] );
+			// Normalize IDs to strings for consistent comparison.
+			$field_ids = array_map( 'strval', $field_ids );
+
+			if ( ! in_array( (string) $field->id, $field_ids, true ) ) {
+				return $placeholder;
+			}
+		}

Also, if there are code paths where this filter is called without a $form value, form_id scoping will be skipped entirely; it’s worth confirming the filter’s usage in GC OpenAI to ensure that’s acceptable.


I'll verify the review comment by examining the codebase to understand the issue with rgar() function and the field object handling.

I've confirmed from Gravity Forms documentation that the rgar() function is used to retrieve a value from an array, while rgobj() is used to retrieve a property value from an object. This validates the core of the review comment. Now I need to verify the codebase specifics.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for filter_loading_text calls and hook registration
rg -n "filter_loading_text" --type=php

Length of output: 279


🏁 Script executed:

#!/bin/bash
# Find the Stream class to check if it has an 'id' property
fd -e php | xargs grep -l "class Stream" | head -5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for add_filter/add_action calls that might register this filter
rg -n "add_filter.*placeholder" --type=php -B2 -A2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for gcoai-stream-loading-text-animation.php to understand filter registration
fd "gcoai-stream-loading-text-animation.php"

Length of output: 49


Let me examine the file content and find the Stream class definition:


---

🏁 Script executed:

```shell
#!/bin/bash
# Read the full file to understand context and the filter registration
cat gc-openai/gcoai-stream-loading-text-animation.php

Length of output: 6227


🏁 Script executed:

#!/bin/bash
# Search for Stream class more broadly
rg -n "class.*Stream" --type=php -A5

Length of output: 560


🏁 Script executed:

#!/bin/bash
# Search for where the filter gform_gcoai_field_loading_text is called/applied
rg -n "gform_gcoai_field_loading_text" --type=php -B3 -A3

Length of output: 603


🏁 Script executed:

#!/bin/bash
# Check if rgar function is defined in codebase or if it's from Gravity Forms
rg -n "function rgar\|function rgobj" --type=php

Length of output: 0


Now I need to verify the behavior of rgar() when passed an object, and confirm the Stream class structure. Let me search for more context:





🏁 Script executed:

#!/bin/bash
# Search for any composer.json or dependency declarations to understand GC_OpenAI origin
fd "composer.json\|package.json\|requirements.php" -type f

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Look for any GC_OpenAI class definitions or where Stream is defined
rg -n "GC_OpenAI.*Fields.*Stream" --type=php -B2 -A10

Length of output: 1226


🏁 Script executed:

#!/bin/bash
# Check if there are any other files that reference Stream or use field->id directly
rg -n "\$field->id" --type=php -C2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for filter callers to understand who calls gform_gcoai_field_loading_text
rg -n "apply_filters.*gform_gcoai_field_loading_text\|do_filter.*gform_gcoai_field_loading_text" --type=php -B3 -A3

Length of output: 0


🌐 Web query:

Gravity Forms rgar function behavior with objects instead of arrays

💡 Result:

Short answer: rgar() expects arrays — it checks array keys and will not read object properties. For Gravity Forms objects (fields, etc.) use rgobj() or object property access (->), or cast the object to an array first.

Why and examples:

  • rgar() is documented as “Get a specific property of an array” and is intended for associative arrays (returns null if key missing). [1]
  • For objects Gravity Forms provides rgobj() to get an object property. Many GF structures (fields since v1.9) use object notation ($field->inputs). [2][3]

Examples:

  • Array (use rgar):
    $value = rgar($array, 'key'); // works. [1]

  • Object (use rgobj or ->):
    $value = rgobj($field, 'inputs'); // recommended. [2]
    // or
    $value = isset($field->inputs) ? $field->inputs : null; // direct object access. [3]

Alternative: cast object to array if you must use rgar:
$value = rgar((array)$obj, 'property');

References:
[1] rgar() docs.
[2] rgobj() docs.
[3] Gravity Forms v1.9 developer notes (object notation / field objects).


Fix the field_id targeting by accessing the field object property directly, not via rgar()

The review comment is accurate. The rgar() function is designed for arrays and will not properly read object properties. Since $field is confirmed to be an instance of \GC_OpenAI\Fields\Stream (an object), using rgar($field, 'id') will fail to access the id property. This means field_id targeting never matches.

The suggested fix is correct:

  • Replace rgar($field, 'id') with $field->id
  • Add type normalization and strict comparison as shown
  • The form parameter concern is also valid (can be null, so form_id filtering is skipped when form is not provided)

Apply the exact diff from the review comment to fix the bug at lines 131–140.

🧰 Tools
🪛 GitHub Check: PHPCS (Files Changed)

[failure] 139-139:
Whitespace found at end of line


[failure] 131-131:
Whitespace found at end of line


[failure] 123-123:
Whitespace found at end of line

🤖 Prompt for AI Agents
In gc-openai/gcoai-stream-loading-text-animation.php around lines 119 to 141,
the field_id targeting uses rgar($field, 'id') which fails because $field is an
object; replace the rgar() call with direct object property access ($field->id),
normalize field_id(s) to an array and compare using strict comparison (===) to
ensure types match, and keep the existing guard that skips field_id filtering if
$this->args['field_id'] is null so behavior when $form is null remains
unchanged.


private function get_styles_css() {
$base = esc_attr( $this->args['base_color'] );
$shimmer = esc_attr( $this->args['shimmer_color'] );
$dur = esc_attr( $this->args['shimmer_duration'] );

Check failure on line 147 in gc-openai/gcoai-stream-loading-text-animation.php

View workflow job for this annotation

GitHub Actions / PHPCS (Files Changed)

Whitespace found at end of line
return
".shimmer-wrapper { display: inline-flex; align-items: center; gap: 8px; } " .
".shimmer-spinner { flex-shrink: 0; } " .
".spinner-rotate { transform-origin: center; animation: spinner-rotation 2s linear infinite; } " .
".spinner-rotate circle { stroke-linecap: round; animation: spinner-stroke 1.5s ease-in-out infinite; } " .
"@keyframes spinner-rotation { 100% { transform: rotate(360deg); } } " .
"@keyframes spinner-stroke { 0% { stroke-dasharray: 0 150; stroke-dashoffset: 0; } 47.5% { stroke-dasharray: 42 150; stroke-dashoffset: -16; } 95%, 100% { stroke-dasharray: 42 150; stroke-dashoffset: -59; } } " .
".shimmer-text { display: inline-block; color: {$base}; line-height: 1.2; } " .
".shimmer { display: inline-block; color: {$base}; line-height: 1.2; background: {$base} linear-gradient(to left, {$base}, {$shimmer} 50%, {$base}); background-position: -4rem top; background-repeat: no-repeat; background-size: 4rem 100%; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; animation: shimmer {$dur} infinite; } " .
"@keyframes shimmer { 0% { background-position: -4rem top; } 70%, 100% { background-position: 12.5rem top; } }";
}
}

# Configuration

new GCOAI_Loading_Animation( array(
'text' => 'Thinking...',
'base_color' => '#292929',
'shimmer_color' => '#fff',
'shimmer_duration' => '2.2s',
'show_shimmer' => true,
'show_spinner' => true,
'spinner_size' => '16',
// 'form_id' => 123, // Uncomment and set to target specific form(s): 123 or array( 18, 22, 35 )
// 'field_id' => 4, // Uncomment and set to target specific field(s): 5 or array( 5, 7, 12 )
) );