diff --git a/lang/en/hub.php b/lang/en/hub.php
index 77815901af4..45384b972b3 100644
--- a/lang/en/hub.php
+++ b/lang/en/hub.php
@@ -24,6 +24,7 @@
*/
$string['activeparticipantnumberaverage'] = 'Average number of recently active participants ({$a})';
$string['activeusersnumber'] = 'Number of recently active users ({$a})';
+$string['aiusagedata'] = 'AI usage data';
$string['aiusagestats'] = 'AI usage stats ({$a->timefrom} - {$a->timeto})';
$string['analyticsactions'] = 'Number of actions taken on generated predictions ({$a})';
$string['analyticsactionsnotuseful'] = 'Number of actions marking a prediction as not useful ({$a})';
@@ -34,6 +35,7 @@ $string['audience_help'] = 'Select the intended audience for the course.';
$string['audienceeducators'] = 'Educators';
$string['audiencestudents'] = 'Students';
$string['audienceadmins'] = 'Moodle administrators';
+$string['average_time'] = 'Average processing time ({$a})';
$string['badgesnumber'] = 'Number of badges ({$a})';
$string['communityremoved'] = 'That course link has been removed from your list';
$string['confirmregistration'] = 'Confirm registration';
@@ -66,6 +68,7 @@ $string['errorwstokenreset'] = '{$a}. Registration token on this site has been r
$string['errorregistrationupdate'] = 'An error occurred during registration update ({$a})';
$string['experttipsandinsights'] = 'Expert tips and insights';
$string['experttipsandinsightsdesc'] = 'Yes, sign me up for exclusive tips, strategies, and insights from Moodle experts and Certified Partners. Update your preferences or unsubscribe any time, as covered in our Privacy Notice.';
+$string['fail_count'] = 'Fail count ({$a})';
$string['geolocation'] = 'Geolocation';
$string['geolocation_help'] = 'In future we may provide location-based searching. If you want to specify the location for your course use a latitude/longitude value here (eg: -31.947884,115.871285). One way to find this is to use Google Maps.';
$string['imageurl'] = 'Image URL';
@@ -80,6 +83,7 @@ $string['licence_link'] = 'licenses';
$string['logourl'] = 'Logo URL';
$string['mobilenotificationsenabled'] = 'Mobile notifications enabled ({$a})';
$string['mobileservicesenabled'] = 'Mobile services enabled ({$a})';
+$string['models'] = 'Models';
$string['modulenumberaverage'] = 'Average number of course modules ({$a})';
$string['mustselectsubject'] = 'You must select a subject';
$string['name'] = 'Name';
@@ -87,6 +91,7 @@ $string['name_help'] = 'This name will be showing in the course listing.';
$string['neverchecked'] = 'Never checked';
$string['next'] = 'Next';
$string['no'] = 'No';
+$string['noaiusagedata'] = 'AI usage data (none)';
$string['nocheckstatusfromunreghub'] = 'The site is not registered on the hub so the status can not be checked.';
$string['nohubselected'] = 'No hub selected';
$string['none'] = 'None';
@@ -98,6 +103,7 @@ $string['policyagreeddesc'] = 'I agree to the Pri
$string['postaladdress'] = 'Postal address';
$string['postaladdress_help'] = 'Postal address of this site, or of the entity represented by this site.';
$string['postsnumber'] = 'Number of posts ({$a})';
+$string['predominant_error'] = 'Predominant HTTP error ({$a})';
$string['previousregistrationdeleted'] = 'The previous registration has been deleted from {$a}. You can restart the registration process. Thank you.';
$string['primaryauthtype'] = 'Primary authentication type ({$a})';
$string['questionsnumber'] = 'Number of questions ({$a})';
@@ -184,8 +190,12 @@ $string['siteurl_help'] = 'The URL is the address of this site. If privacy sett
$string['siteversion'] = 'Moodle version';
$string['siteversion_help'] = 'The Moodle version of this site.';
$string['skipregistration'] = 'Skip';
+$string['success_count'] = 'Success count ({$a})';
$string['subject'] = 'Subject';
$string['subject_help'] = 'Select the main subject area which the course covers.';
+$string['timefrom'] = 'Time from ({$a})';
+$string['timeto'] = 'Time to ({$a})';
+$string['time_range'] = 'Time range';
$string['type'] = 'Shared';
$string['unregister'] = 'Unregister';
$string['unregistrationerror'] = 'An error occurred while attempting to unregister the site: {$a}';
diff --git a/lib/classes/hub/registration.php b/lib/classes/hub/registration.php
index 42f891b0288..fd8dadd1cc7 100644
--- a/lib/classes/hub/registration.php
+++ b/lib/classes/hub/registration.php
@@ -239,6 +239,8 @@ class registration {
* @return string
*/
public static function get_stats_summary($siteinfo) {
+ global $OUTPUT;
+
$fieldsneedconfirm = self::get_new_registration_fields();
$summary = html_writer::tag('p', get_string('sendfollowinginfo_help', 'hub')) .
html_writer::start_tag('ul');
@@ -284,7 +286,7 @@ class registration {
'sitetheme' => get_string('sitetheme', 'hub', $siteinfo['sitetheme']),
'primaryauthtype' => get_string('primaryauthtype', 'hub', $siteinfo['primaryauthtype']),
'pluginusage' => get_string('pluginusagedata', 'hub', $pluginusagelinks),
- 'aiusage' => get_string('aiusagestats', 'hub', self::get_ai_usage_time_range(true)),
+ 'aiusage' => $OUTPUT->render_from_template('core/ai_usage_data', ['aiusagedata' => self::show_ai_usage()]),
];
foreach ($senddata as $key => $str) {
@@ -725,6 +727,97 @@ class registration {
];
}
+ /**
+ * Displays AI usage data for all providers.
+ *
+ * @return array Array containing usage data, grouped by provider
+ */
+ public static function show_ai_usage(): array {
+ // Initialize aiusage collection.
+ $aiusage = [];
+
+ // Process each provider's data.
+ foreach (self::get_ai_usage_data() as $provider => $actions) {
+ if ($provider === 'time_range') {
+ $aiusage['timerange'] = [
+ 'label' => get_string($provider, 'hub'),
+ 'values' => self::format_ai_usage_actions($actions),
+ ];
+ } else {
+ // Initialize provider data structure.
+ $aiusage['providers'][] = [
+ 'providername' => get_string('pluginname', $provider),
+ 'aiactions' => self::format_ai_usage_actions($actions),
+ ];
+ }
+ }
+
+ return $aiusage;
+ }
+
+ /**
+ * Formats individual actions for a provider.
+ *
+ * @param array $actions Raw actions data
+ * @return array Formatted action data
+ */
+ private static function format_ai_usage_actions(array $actions): array {
+ $formattedactions = [];
+
+ foreach ($actions as $action => $values) {
+ if (in_array($action, ['timefrom', 'timeto'])) {
+ $formattedactions[] = get_string($action, 'hub', userdate($values));
+ } else {
+ $formattedactions[] = [
+ 'actionname' => get_string("action_$action", 'core_ai'),
+ 'aiactionvalues' => self::format_ai_usage_action_values($values),
+ ];
+ }
+ }
+
+ return $formattedactions;
+ }
+
+ /**
+ * Formats action values into formatted strings.
+ *
+ * @param array $values Action values to format
+ * @return array Formatted action values
+ */
+ private static function format_ai_usage_action_values(array $values): array {
+ $formattedvalues = [];
+
+ foreach ($values as $key => $value) {
+ if (get_string_manager()->string_exists($key, 'hub')) {
+ if ($key === 'models') {
+ $formattedvalues[] = [
+ 'label' => get_string($key, 'hub'),
+ 'models' => self::format_ai_usage_model_values($value),
+ ];
+ } else {
+ $formattedvalues[]['values'] = get_string($key, 'hub', $value);
+ }
+ }
+ }
+
+ return $formattedvalues;
+ }
+
+ /**
+ * Formats model values with their counts.
+ *
+ * @param array $values Formatted model values
+ */
+ private static function format_ai_usage_model_values(array $values): array {
+ $modelvalues = [];
+
+ foreach ($values as $model => $count) {
+ $modelvalues[] = "$model ($count[count])";
+ }
+
+ return $modelvalues;
+ }
+
/**
* Get AI usage data.
*
diff --git a/lib/templates/ai_usage_data.mustache b/lib/templates/ai_usage_data.mustache
new file mode 100644
index 00000000000..b86d50239e2
--- /dev/null
+++ b/lib/templates/ai_usage_data.mustache
@@ -0,0 +1,93 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/ai_usage_data
+
+ Template for AI usage data.
+
+ Example context (json):
+ {
+ "aiusagedata": [{
+ "providers": [{
+ "providerName": "AI provider name",
+ "aiactions": [{
+ "actionName": "AI action name",
+ "aiactionvalues": [{
+ "success_count": "1",
+ "fail_count": "2",
+ "average_time": "15",
+ "predominant_error": "403",
+ "models": [{
+ "gpt-4o": "1"
+ }]
+ }]
+ }]
+ }],
+ "timerange": [{
+ "label": "Time range for stats",
+ "values": [{
+ "timefrom": "Time from: Friday, 2 May 2025, 1:22 AM",
+ "timeto": "Time from: Friday, 3 May 2025, 1:22 AM"
+ }]
+ }]
+ }]
+ }
+}}
+
+{{#aiusagedata}}
+ {{#str}} aiusagedata, hub {{/str}}:
+
+ {{! AI providers. }}
+ {{#providers}}
+ - {{providername}}:
+ {{#aiactions}}
+
+ - {{actionname}}:
+ {{#aiactionvalues}}
+
+ {{#values}}
+ - {{values}}
+ {{/values}}
+
+ {{^values}}
+ - {{label}}:
+ {{#models}}
+
+ {{/models}}
+ {{/values}}
+
+ {{/aiactionvalues}}
+
+ {{/aiactions}}
+ {{/providers}}
+
+ {{! Time range for stats. }}
+ {{#timerange}}
+ - {{label}}:
+ {{#values}}
+
+ {{/values}}
+ {{/timerange}}
+
+{{/aiusagedata}}
+{{^aiusagedata}}
+ {{#str}} noaiusagedata, hub {{/str}}
+{{/aiusagedata}}
diff --git a/lib/tests/hub/registration_test.php b/lib/tests/hub/registration_test.php
index a76b725ad0f..22956fe2fef 100644
--- a/lib/tests/hub/registration_test.php
+++ b/lib/tests/hub/registration_test.php
@@ -90,16 +90,44 @@ final class registration_test extends \advanced_testcase {
* Test the AI usage data is calculated correctly.
*/
public function test_get_ai_usage(): void {
- global $CFG, $DB;
$this->resetAfterTest();
- $clock = $this->mock_clock_with_frozen();
+ $clock = $this->mock_clock_with_frozen(1700000000);
+ $this->generate_ai_usage_data();
+
+ // Get our site info and check the expected calculations are correct.
+ $siteinfo = registration::get_site_info();
+ $aisuage = json_decode($siteinfo['aiusage']);
+ // Check generated text.
+ $this->assertEquals(1, $aisuage->aiprovider_openai->generate_text->success_count);
+ $this->assertEquals(0, $aisuage->aiprovider_openai->generate_text->fail_count);
+ // Check generated images.
+ $this->assertEquals(2, $aisuage->aiprovider_openai->generate_image->success_count);
+ $this->assertEquals(3, $aisuage->aiprovider_openai->generate_image->fail_count);
+ $this->assertEquals(15, $aisuage->aiprovider_openai->generate_image->average_time);
+ $this->assertEquals(403, $aisuage->aiprovider_openai->generate_image->predominant_error);
+ // Check time range is set correctly.
+ $this->assertEquals($clock->time() - WEEKSECS, $aisuage->time_range->timefrom);
+ $this->assertEquals($clock->time(), $aisuage->time_range->timeto);
+ // Check model counts.
$gpt4omodel = 'gpt-4o';
$dalle3model = 'dall-e-3';
+ $this->assertEquals(1, $aisuage->aiprovider_openai->generate_text->models->{$gpt4omodel}->count);
+ $this->assertEquals(2, $aisuage->aiprovider_openai->generate_image->models->{$dalle3model}->count);
+ $this->assertEquals(3, $aisuage->aiprovider_openai->generate_image->models->unknown->count);
+ }
+
+ /**
+ * Create some dummy AI usage data.
+ */
+ private function generate_ai_usage_data(): void {
+ global $DB;
+
+ $clock = $this->mock_clock_with_frozen(1700000000);
// Record some generated text.
$record = new \stdClass();
- $record->provider = 'openai';
+ $record->provider = 'aiprovider_openai';
$record->actionname = 'generate_text';
$record->actionid = 1;
$record->userid = 1;
@@ -107,14 +135,14 @@ final class registration_test extends \advanced_testcase {
$record->success = true;
$record->timecreated = $clock->time() - 5;
$record->timecompleted = $clock->time();
- $record->model = $gpt4omodel;
+ $record->model = 'gpt-4o';
$DB->insert_record('ai_action_register', $record);
// Record a generated image.
$record->actionname = 'generate_image';
$record->actionid = 111;
$record->timecreated = $clock->time() - 20;
- $record->model = $dalle3model;
+ $record->model = 'dall-e-3';
$DB->insert_record('ai_action_register', $record);
// Record another image.
$record->actionid = 222;
@@ -134,24 +162,38 @@ final class registration_test extends \advanced_testcase {
$record->actionid = 6;
$record->errorcode = 404;
$DB->insert_record('ai_action_register', $record);
+ }
- // Get our site info and check the expected calculations are correct.
- $siteinfo = registration::get_site_info();
- $aisuage = json_decode($siteinfo['aiusage']);
- // Check generated text.
- $this->assertEquals(1, $aisuage->openai->generate_text->success_count);
- $this->assertEquals(0, $aisuage->openai->generate_text->fail_count);
- // Check generated images.
- $this->assertEquals(2, $aisuage->openai->generate_image->success_count);
- $this->assertEquals(3, $aisuage->openai->generate_image->fail_count);
- $this->assertEquals(15, $aisuage->openai->generate_image->average_time);
- $this->assertEquals(403, $aisuage->openai->generate_image->predominant_error);
- // Check time range is set correctly.
- $this->assertEquals($clock->time() - WEEKSECS, $aisuage->time_range->timefrom);
- $this->assertEquals($clock->time(), $aisuage->time_range->timeto);
- // Check model counts.
- $this->assertEquals(1, $aisuage->openai->generate_text->models->{$gpt4omodel}->count);
- $this->assertEquals(2, $aisuage->openai->generate_image->models->{$dalle3model}->count);
- $this->assertEquals(3, $aisuage->openai->generate_image->models->unknown->count);
+ /**
+ * Test the show AI usage data.
+ */
+ public function test_show_ai_usage(): void {
+ $this->resetAfterTest();
+
+ // Init the registration class.
+ $registration = new registration();
+
+ // There should be no data to show yet.
+ $aisuagedata = $registration->show_ai_usage();
+ $this->assertTrue(empty($aisuagedata));
+
+ // After generating some data, there should now be some data to show.
+ $this->generate_ai_usage_data();
+ $aisuagedata = $registration->show_ai_usage();
+ $this->assertTrue(!empty($aisuagedata));
+
+ foreach ($aisuagedata['providers'] as $provider) {
+ $this->assertEquals('OpenAI API provider', $provider['providername']);
+ $this->assertTrue(!empty($provider['aiactions']));
+
+ foreach ($provider['aiactions'] as $action) {
+ $actionname = $action['actionname'];
+ $this->assertTrue(!empty($actionname));
+ }
+ }
+
+ $timerange = $aisuagedata['timerange'];
+ $this->assertEquals(get_string('time_range', 'hub'), $timerange['label']);
+ $this->assertTrue(!empty($timerange['values']));
}
}