How to call an extension's controller action inside a extension widget?

Hi!

I want to thank the Yii community, to help me learning this amazing framework, by contribute with a GPT extension, super easy to use. Today was my day, and I made my first demo extension.

But have to clear some misunderstanding. My scenario is like this in my gptagent7 extension:

I have a controller rdng\gptagent7\controllers\ChatController and a widget rdng\gptagent7\widgets\ChatBot. In my ChatBot widget I got this piece of code:

            // Button
            $return .= Html::a('test', ['chat/chat']);
            $return .= Html::button($this->buttonTitle, ['onclick' => "
                let question = $('#chatInput').val();
                
                $.ajax({
                    type     :'post',
                    cache    : false,
                    data     : {
                        question: question,
                    },
                    url  : '".Url::to(['/chat/chat'])."',
                    success  : function(response) {
                        $('#messages').html(response);
                    },
                    error  : function(response) {
                        alert(response);
                    }
                });
                return false;
            ", 'class' => $this->buttonClass]);

Of course the ChatBot lives inside a Yii2 app.

I try the standard approach I’m used to when doing ajax calls i.e. Url::to('/chat/chat'), but this calls the β€œmain” app’s controller.

What I must do, to call in my widget the ChatWidget the actionChat()?

I found some example, but I don’t know if this is correct approach, thus I don’t know how to replace Url::to(…) with this request:

use yii\helpers\Url;
use yii\httpclient\Client;

// ...

// Generate the URL for the extension's controller action
$url = Url::to(['/my-extension/my-action']); // Adjust the route accordingly

// Create an HTTP client instance
$client = new Client();

// Make a POST request to the extension's controller action
$response = $client->createRequest()
    ->setMethod('POST')
    ->setUrl($url)
    ->send();

// Check the response
if ($response->isOk) {
    // Action was successfully called
    $content = $response->content;
    // Further processing...
} else {
    // Error handling
    echo 'Error: ' . $response->statusCode;
}

If there is a simpler way to do it, please help me.
Thanks and have a nice day!

If I understand you want to call your controller from the view/widget using ajax.
If that is true, then just call it the same way you could call anything else.

But since you want to have controllers, views et al. I suggest you create a module instead. Widgets are made to be just widgets nothing more.

Check out this guide on modules:

1 Like

Thanks for your reply. So far i’ve already tried to create a module within the extension like this:

<?php

namespace rdng\gptagent7;

use yii\base\Module;

class GptAgent7 extends Module {
    
    public $controllerNamespace = 'rdng\gptagent7\controllers';
    public $widgetNamespace = 'rdng\gptagent7\widgets';

    public function init() {
        parent::init();
    }
}

And register this module in web config:

    'modules' => [
        'gptagent7' => [
            'class' => 'rdng\gptagent7\GptAgent7'
        ]
    ],

But when I use this Widget in an Yii2 app in a view (site/index) like this:

         <p class="lead"><?php 

            echo ChatBot::widget([]);
        
        ?></p>

The widgets button onclick ajax call gives me an 404 error:

            $url = Url::to(['/gptagent7/chat']);
            $return .= Html::a('test', $url);
            $return .= Html::button($this->buttonTitle, ['onclick' => "
                let question = $('#chatInput').val();
                // alert('".$url."');
                
                $.ajax({
                    type     :'post',
                    cache    : false,
                    data     : {
                        question: question,
                    },
                    url  : '".$url."',
                    success  : function(response) {
                        $('#messages').html(response);
                    },
                    error  : function(response) {
                        alert(response.toString());
                    }
                });
                return false;
            ", 'class' => $this->buttonClass]);

If someone can point out what i’m missing?

@evstevemd If I read you, you suggest to build the frontend of ChatBot in a module instead of building an Widget although the widget is included in the widget?

My idea was to build a Widget that can be fully custom to fit any needs and I was expecting to use by calling it on different views in main applications. I also included helpers. I know how to call a module view, but what if I want just the ChatBot for example in another view (not module view, but let’s say in main app view).

The only problem that i’m facing is that ajax call inside the widget is calling the main app controllers/actions.

That is right, according to your question that is a way to go. Else where do you put the controller and related code, and the widget/view code?

No need for a controller then. Make widget config to receive URL as config parameter, then call that one. That assumes your widget is not attached by any mean with the controller being called. That way you can create a Widget that can be used by arbitrary apps

I develop few Yii2 projects and have the need to use that view/Widget. Sometimes is a widget itself and sometimes a view.

Create it in github or somehwere if it is open source and then we can suggest. If it closed source you have to make a sample for someone to look into and help!

Ok I understand. But I have this input and a buton in Widget which sends data to an GPT API, and I wanted to use ajax calls for this. I usually do this with easy, but in this situation I’m kind of stucked. I will try to make a module, but I see Widgets are way more customizable.

Have to redesign my idea.

Yeah great idea. Will do it.

If all you do with widget is calling GPT then just call it like you would anything. But then you will have the keys exposed and other sensitive data

Yes, it is easy to say, but the Widget resides in the extension itself, where is also the controller with the action which I want to call with ajax. But as I mentioned the Url::to() helper generates the link for the app where this Widget is called.

For keys and other private data will be done through component/params configuration.

Here you can see how are my directory organized: https://racunalnisko-drustvo.si/img/gpt.png

1 Like

To reuse the widget, just make sure that it does not depend on app components, unless they will be shipped with the widget. All input and parameters should be passed as configs.
That is all!

The Widget is stand alone. I mean by input the textfield to interact with GPT. The widget is not that much complicated. If would be not for the issue I’m facing will be already done.

<?php

namespace rdng\gptagent7\widgets;

use yii\helpers\Html;
use yii\helpers\Url;

class ChatBot extends \yii\base\Widget
{
    public $system = 'You are a helpful assistant.';
    public $user = 'Hello.';
    public $model = 'gpt-4-1106-preview';
    public $maxtokens = 4086;
    public $containerClass = '';
    public $inputClass = '';
    public $messagesClass = '';
    public $systemMessageClass = '';
    public $userMessageClass = '';
    public $buttonTitle = 'Send';
    public $buttonClass = '';

    public function run()
    {
        $return = '';

        $controller = $this->getView()->context;

        $return .= $controller->id;

        // Container
        $return .= Html::beginTag('div', ['class' => $this->containerClass]);

            // Messages
            $return .= Html::beginTag('div', ['id' => 'messages', 'class' => $this->messagesClass]);
            
            $return .= Html::endTag('div');

            // Input
            $return .= Html::input('text', 'gptInput', null, ['id' => 'chatInput', 'class' => $this->inputClass]);
            
            // Button
            $url = Url::to(['/gptagent7/chat']);
            // $url = Yii::$app->get('chat')->runAction('chat');
            $return .= Html::a('test', $url);
            $return .= Html::button($this->buttonTitle, ['onclick' => "
                let question = $('#chatInput').val();
                // alert('".$url."');
                
                $.ajax({
                    type     :'post',
                    cache    : false,
                    data     : {
                        question: question,
                    },
                    url  : '".$url."',
                    success  : function(response) {
                        $('#messages').html(response);
                    },
                    error  : function(response) {
                        alert(response.toString());
                    }
                });
                return false;
            ", 'class' => $this->buttonClass]);
            
        $return .= Html::endTag('div');
        // $api_key = Yii::$app->params['api-key'];

        // Return
        return $return;
        // return $return['choices'][0]['message']['content'];
    }
}

The good news is that is working as expected using the module’s view and inside the view the same damn Widget.

But still have to figure it out how to integrate only the Widget in the main app with the Send button and textInput calling the extensions controller’s action.

 // Button
 $url = Url::to(['/gptagent7/chat']);

Here is your problem. You are hard coding the URL. It should be one of the configurations received from the outside. That will make it flexible and better decoupled

1 Like

Man you opened my eyes. Thanks.
That is one approach, but I was giving just a wrong route.
Anyway without the module it would not work.

$url = Url::to(['/gptagent7/chat/question']);

Now works like a charm!
Hope anyone helps this post!

You are welcome!

Even with module approach you are not going to know what module ID user is going to call it and hence affecting the URL. Unless you want to handle the complexities around URLs I suggest you let the user configure module ID as well as url. In both case providing default value for those who will follow ya convention!

1 Like

Hey I’m stucked in a situation. My extension works fine so far, but when I want to test it in my β€œreal life” application, and I want to integrate the widget in it, i get this error:

Exception (Not instantiable) 'yii\di\NotInstantiableException' with message 'Failed to instantiate component or class "rdng\gptagent7\GptAgent7".' 

in /var/www/html/eco-new/vendor/yiisoft/yii2/di/Container.php:509

Sounds familiar? I don’t use composer cause I just copy the copy loccaly to test it. I dida a module configuration in web config:

'modules' => [
        'gptagent7' => [
            'class' => 'rdng\gptagent7\GptAgent7'
        ]
    ],

and in composer i got (although I copy it manually):

"require-dev": {
        ...
        "rdng/yii2-gpt-agent7": "@dev"
    },

I got so far:

            $return .= Html::tag('div', Icon::show('comments'), ['class' => 'chatbot', 'onclick' => '$("#chatbot-popup").fadeToggle(); return false;', 'title' => T::app('Help assistant')]);
            $return .= Html::beginTag('div', ['id' => 'chatbot-popup', 'class' => 'chatbot-popup']);
            
            Yii::$classMap['rdng\gptagent7\GptAgent7'] = '../vendor/rdng/yii2-gpt-agent7/GptAgent7.php';
            Yii::$classMap['rdng\gptagent7\helpers\GptAssistantHelper'] = '../vendor/rdng/yii2-gpt-agent7/helpers/GptAssistantHelper.php';
            Yii::$classMap['rdng\gptagent7\models\Gpt'] = '../vendor/rdng/yii2-gpt-agent7/models/Gpt.php';
            Yii::$classMap['rdng\gptagent7\widgets\AssistantBot'] = '../vendor/rdng/yii2-gpt-agent7/widgets/AssistantBot.php';
            Yii::$classMap['rdng\gptagent7\assets\AppAsset'] = '../vendor/rdng/yii2-gpt-agent7/assets/AppAsset.php';

            // Retrive assistant
            $assistant = GptAssistantHelper::Retrieve('asst_d5WEDzaoXhUtcR88s63whMu0');

            // Create new thread
            $thread = GptAssistantHelper::CreateThread();

            $return .= AssistantBot::widget([
                'headerTitle' => $assistant['name'],
                'assistantId' => $assistant['id'],
                'threadId' => $thread['id'],
            ]);
            $return .= Html::endTag('div');

Files:

/var/www/html/gpt-agent7/runtime/tmp-extensions/yii2-gpt-agent7
β”œβ”€β”€ assets
β”‚   └── AppAsset.php
β”œβ”€β”€ composer.json
β”œβ”€β”€ composer.lock
β”œβ”€β”€ controllers
β”‚   β”œβ”€β”€ AssistantController.php
β”‚   β”œβ”€β”€ AudioController.php
β”‚   └── ChatController.php
β”œβ”€β”€ css
β”‚   └── chat.css
β”œβ”€β”€ GptAgent7.php
β”œβ”€β”€ helpers
β”‚   β”œβ”€β”€ GptAssistantHelper.php
β”‚   β”œβ”€β”€ GptAudioHelper.php
β”‚   β”œβ”€β”€ GptChatHelper.php
β”‚   └── GptDalleHelper.php
β”œβ”€β”€ models
β”‚   β”œβ”€β”€ GptAssistant.php
β”‚   β”œβ”€β”€ GptAudio.php
β”‚   β”œβ”€β”€ GptChat.php
β”‚   β”œβ”€β”€ GptDalle.php
β”‚   └── Gpt.php
β”œβ”€β”€ README.md
└── widgets
    β”œβ”€β”€ AssistantBot.php
    β”œβ”€β”€ AudioBot.php
    β”œβ”€β”€ ChatBot.php
    └── DalleBot.php

Thanks.
Luka