Compare commits

..
1 Commits
Author SHA1 Message Date
Wojtek 686e927249 Add Gemini API as an AI provider 2026-10-16 22:41:08 +02:00
13 changed files with 89 additions and 28 deletions
+1 -1
View File
@@ -30,6 +30,6 @@ Szkielet komponentu Joomla 6 do gromadzenia pytań testowych.
## Instalacja ## Instalacja
W Joomla wybierz **System → Install → Install from Folder** i wskaż ten katalog. Manifestem instalacyjnym jest `com_testquestions.xml` (wersja 1.43.27). W Joomla wybierz **System → Install → Install from Folder** i wskaż ten katalog. Manifestem instalacyjnym jest `com_testquestions.xml` (wersja 1.43.28).
Po instalacji komponent jest dostępny w panelu administratora jako **Pytania testowe**. Po instalacji komponent jest dostępny w panelu administratora jako **Pytania testowe**.
@@ -6,6 +6,7 @@
<option value="openai">OpenAI</option> <option value="openai">OpenAI</option>
<option value="grok">Grok</option> <option value="grok">Grok</option>
<option value="groq">Groq</option> <option value="groq">Groq</option>
<option value="gemini">Gemini</option>
</field> </field>
<field name="ai_api_key" type="aiapikey" addfieldprefix="Komponent\Component\Testquestions\Administrator\Field" label="COM_TESTQUESTIONS_AI_API_KEY" filter="raw" autocomplete="off" size="120" class="w-100" /> <field name="ai_api_key" type="aiapikey" addfieldprefix="Komponent\Component\Testquestions\Administrator\Field" label="COM_TESTQUESTIONS_AI_API_KEY" filter="raw" autocomplete="off" size="120" class="w-100" />
</fieldset> </fieldset>
@@ -72,9 +72,11 @@ COM_TESTQUESTIONS_AI_API_KEY="Klucz API"
COM_TESTQUESTIONS_AI_OPENAI_KEY="Klucz API OpenAI" COM_TESTQUESTIONS_AI_OPENAI_KEY="Klucz API OpenAI"
COM_TESTQUESTIONS_AI_GROK_KEY="Klucz API Grok" COM_TESTQUESTIONS_AI_GROK_KEY="Klucz API Grok"
COM_TESTQUESTIONS_AI_GROQ_KEY="Klucz API Groq" COM_TESTQUESTIONS_AI_GROQ_KEY="Klucz API Groq"
COM_TESTQUESTIONS_AI_GEMINI_KEY="Klucz API Gemini"
COM_TESTQUESTIONS_AI_OPENAI_MODEL="Model OpenAI" COM_TESTQUESTIONS_AI_OPENAI_MODEL="Model OpenAI"
COM_TESTQUESTIONS_AI_GROK_MODEL="Model Grok" COM_TESTQUESTIONS_AI_GROK_MODEL="Model Grok"
COM_TESTQUESTIONS_AI_GROQ_MODEL="Model Groq API" COM_TESTQUESTIONS_AI_GROQ_MODEL="Model Groq API"
COM_TESTQUESTIONS_AI_GEMINI_MODEL="Model Gemini"
COM_TESTQUESTIONS_AI_MODEL_INVALID="Wybierz prawidłowy model dla każdego dostawcy AI." COM_TESTQUESTIONS_AI_MODEL_INVALID="Wybierz prawidłowy model dla każdego dostawcy AI."
COM_TESTQUESTIONS_AI_TOKEN_USAGE="Wykorzystanie tokenów API" COM_TESTQUESTIONS_AI_TOKEN_USAGE="Wykorzystanie tokenów API"
COM_TESTQUESTIONS_AI_TOKEN_USAGE_INFO="Suma użycia zapisanego przy ocenach AI we wszystkich wynikach testów. Powtórne oceny są zliczane, jeśli zostały wykonane po tej aktualizacji." COM_TESTQUESTIONS_AI_TOKEN_USAGE_INFO="Suma użycia zapisanego przy ocenach AI we wszystkich wynikach testów. Powtórne oceny są zliczane, jeśli zostały wykonane po tej aktualizacji."
@@ -0,0 +1 @@
-- Wydanie 1.43.28: dodano dostawcę Gemini API.
@@ -36,13 +36,13 @@ class AiController extends BaseController
$input = Factory::getApplication()->getInput(); $input = Factory::getApplication()->getInput();
$provider = strtolower($input->post->getCmd('ai_provider', '')); $provider = strtolower($input->post->getCmd('ai_provider', ''));
if (!in_array($provider, ['openai', 'grok', 'groq'], true)) { if (!in_array($provider, ['openai', 'grok', 'groq', 'gemini'], true)) {
throw new \RuntimeException(Text::_('COM_TESTQUESTIONS_AI_PROVIDER_INVALID')); throw new \RuntimeException(Text::_('COM_TESTQUESTIONS_AI_PROVIDER_INVALID'));
} }
$models = AiService::availableModels(); $models = AiService::availableModels();
$selectedModels = []; $selectedModels = [];
foreach (['openai', 'grok', 'groq'] as $modelProvider) { foreach (['openai', 'grok', 'groq', 'gemini'] as $modelProvider) {
$model = trim($input->post->getString('ai_' . $modelProvider . '_model', '')); $model = trim($input->post->getString('ai_' . $modelProvider . '_model', ''));
if (!isset($models[$modelProvider][$model])) { if (!isset($models[$modelProvider][$model])) {
throw new \RuntimeException(Text::_('COM_TESTQUESTIONS_AI_MODEL_INVALID')); throw new \RuntimeException(Text::_('COM_TESTQUESTIONS_AI_MODEL_INVALID'));
@@ -71,7 +71,7 @@ class AiController extends BaseController
$settings['ai_' . $modelProvider . '_model'] = $model; $settings['ai_' . $modelProvider . '_model'] = $model;
} }
foreach (['openai', 'grok', 'groq'] as $keyProvider) { foreach (['openai', 'grok', 'groq', 'gemini'] as $keyProvider) {
$fieldName = 'ai_' . $keyProvider . '_api_key'; $fieldName = 'ai_' . $keyProvider . '_api_key';
$apiKey = trim((string) $input->post->get($fieldName, '', 'RAW')); $apiKey = trim((string) $input->post->get($fieldName, '', 'RAW'));
if ($apiKey !== '') { if ($apiKey !== '') {
@@ -96,7 +96,7 @@ class ResultModel extends BaseDatabaseModel
$keyName = 'ai_' . $provider . '_api_key'; $keyName = 'ai_' . $provider . '_api_key';
$apiKey = trim((string) $params->get($keyName, $params->get('ai_api_key', ''))); $apiKey = trim((string) $params->get($keyName, $params->get('ai_api_key', '')));
$componentKeyLength = mb_strlen($apiKey); $componentKeyLength = mb_strlen($apiKey);
$defaultModels = ['openai' => 'gpt-4.1-mini', 'grok' => 'grok-4.7', 'groq' => 'openai/gpt-oss-120b']; $defaultModels = ['openai' => 'gpt-4.1-mini', 'grok' => 'grok-4.7', 'groq' => 'openai/gpt-oss-120b', 'gemini' => 'gemini-3.8-flash'];
$model = trim((string) $params->get('ai_' . $provider . '_model', $defaultModels[$provider] ?? '')); $model = trim((string) $params->get('ai_' . $provider . '_model', $defaultModels[$provider] ?? ''));
// Odczytaj zapisane parametry bezpośrednio z bazy. Dzięki temu nie użyjemy // Odczytaj zapisane parametry bezpośrednio z bazy. Dzięki temu nie użyjemy
@@ -18,7 +18,7 @@ class ResultsModel extends ListModel
$item->ai_output_tokens = 0; $item->ai_output_tokens = 0;
$item->ai_total_tokens = 0; $item->ai_total_tokens = 0;
$item->ai_cost_usd = 0.0; $item->ai_cost_usd = 0.0;
$item->ai_tokens_by_provider = ['openai' => 0, 'grok' => 0, 'groq' => 0]; $item->ai_tokens_by_provider = ['openai' => 0, 'grok' => 0, 'groq' => 0, 'gemini' => 0];
$answers = json_decode((string) ($item->answers ?? ''), true); $answers = json_decode((string) ($item->answers ?? ''), true);
if (!is_array($answers)) { if (!is_array($answers)) {
@@ -68,7 +68,7 @@ class ResultsModel extends ListModel
'output_tokens' => 0, 'output_tokens' => 0,
'total_tokens' => 0, 'total_tokens' => 0,
'cost_usd' => 0.0, 'cost_usd' => 0.0,
'providers' => ['openai' => $emptyProviderTotals, 'grok' => $emptyProviderTotals, 'groq' => $emptyProviderTotals], 'providers' => ['openai' => $emptyProviderTotals, 'grok' => $emptyProviderTotals, 'groq' => $emptyProviderTotals, 'gemini' => $emptyProviderTotals],
]; ];
foreach ($answersRows as $answersJson) { foreach ($answersRows as $answersJson) {
@@ -28,6 +28,10 @@ class AiService
'openai/gpt-oss-20b' => 'GPT OSS 20B', 'openai/gpt-oss-20b' => 'GPT OSS 20B',
'llama-3.3-70b-versatile' => 'Llama 3.3 70B Versatile', 'llama-3.3-70b-versatile' => 'Llama 3.3 70B Versatile',
], ],
'gemini' => [
'gemini-3.8-flash' => 'Gemini 3.8 Flash',
'gemini-3.1-pro-preview' => 'Gemini 3.1 Pro (Preview)',
],
]; ];
} }
@@ -38,6 +42,7 @@ class AiService
$config = match ($provider) { $config = match ($provider) {
'grok' => ['url' => 'https://api.x.ai/v1/responses'], 'grok' => ['url' => 'https://api.x.ai/v1/responses'],
'groq' => ['url' => 'https://api.groq.com/openai/v1/chat/completions'], 'groq' => ['url' => 'https://api.groq.com/openai/v1/chat/completions'],
'gemini' => ['url' => 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode($model) . ':generateContent'],
default => ['url' => 'https://api.openai.com/v1/responses'], default => ['url' => 'https://api.openai.com/v1/responses'],
}; };
@@ -45,7 +50,7 @@ class AiService
throw new \RuntimeException('AI_NOT_CONFIGURED'); throw new \RuntimeException('AI_NOT_CONFIGURED');
} }
if (!in_array($provider, ['openai', 'grok', 'groq'], true)) { if (!in_array($provider, ['openai', 'grok', 'groq', 'gemini'], true)) {
throw new \RuntimeException('Nieprawidłowy dostawca AI: ' . $provider); throw new \RuntimeException('Nieprawidłowy dostawca AI: ' . $provider);
} }
@@ -61,29 +66,40 @@ class AiService
. "Odpowiedź ucznia:\n" . ($answer !== '' ? $answer : 'Brak odpowiedzi') . "\n\n" . "Odpowiedź ucznia:\n" . ($answer !== '' ? $answer : 'Brak odpowiedzi') . "\n\n"
. "Zwróć wyłącznie poprawny obiekt JSON w formacie {\"awarded_points\": liczba_całkowita, \"feedback\": \"krótka ocena po polsku\"}. W ocenie opisz poprawne i brakujące elementy. Nie zmieniaj wzorca odpowiedzi."; . "Zwróć wyłącznie poprawny obiekt JSON w formacie {\"awarded_points\": liczba_całkowita, \"feedback\": \"krótka ocena po polsku\"}. W ocenie opisz poprawne i brakujące elementy. Nie zmieniaj wzorca odpowiedzi.";
$requestData = $provider === 'groq' $requestData = match ($provider) {
? [ 'groq' => [
'model' => $model, 'model' => $model,
'messages' => [['role' => 'user', 'content' => $input]], 'messages' => [['role' => 'user', 'content' => $input]],
'temperature' => 0.1, 'temperature' => 0.1,
] ],
: [ 'gemini' => [
'contents' => [['parts' => [['text' => $input]]]],
'generationConfig' => ['responseMimeType' => 'application/json'],
],
default => [
'model' => $model, 'model' => $model,
'input' => $input, 'input' => $input,
]; ],
};
// GPT-5.x reasoning models do not accept a custom temperature. // GPT-5.x reasoning models do not accept a custom temperature.
if ($provider === 'grok' || ($provider === 'openai' && $model === 'gpt-4.1-mini')) { if ($provider === 'grok' || ($provider === 'openai' && $model === 'gpt-4.1-mini')) {
$requestData['temperature'] = 0.1; $requestData['temperature'] = 0.1;
} }
$payload = json_encode($requestData, JSON_UNESCAPED_UNICODE); $payload = json_encode($requestData, JSON_UNESCAPED_UNICODE);
$headers = [
'Content-Type' => 'application/json',
];
if ($provider === 'gemini') {
$headers['x-goog-api-key'] = trim($apiKey);
} else {
$headers['Authorization'] = 'Bearer ' . trim($apiKey);
}
$response = HttpFactory::getHttp()->post( $response = HttpFactory::getHttp()->post(
$config['url'], $config['url'],
$payload, $payload,
[ $headers,
'Authorization' => 'Bearer ' . trim($apiKey),
'Content-Type' => 'application/json',
],
60 60
); );
$body = json_decode((string) $response->body, true); $body = json_decode((string) $response->body, true);
@@ -132,13 +148,15 @@ class AiService
return null; return null;
} }
$inputTokens = max(0, (int) ($usage['input_tokens'] ?? $usage['prompt_tokens'] ?? 0)); $inputTokens = max(0, (int) ($usage['input_tokens'] ?? $usage['prompt_tokens'] ?? $usage['promptTokenCount'] ?? 0));
$outputTokens = max(0, (int) ($usage['output_tokens'] ?? $usage['completion_tokens'] ?? 0)); $outputTokens = max(0, (int) ($usage['output_tokens'] ?? $usage['completion_tokens'] ?? (
if ((!isset($usage['input_tokens']) && !isset($usage['prompt_tokens'])) (int) ($usage['candidatesTokenCount'] ?? 0) + (int) ($usage['thoughtsTokenCount'] ?? 0)
|| (!isset($usage['output_tokens']) && !isset($usage['completion_tokens']))) { )));
if ((!isset($usage['input_tokens']) && !isset($usage['prompt_tokens']) && !isset($usage['promptTokenCount']))
|| (!isset($usage['output_tokens']) && !isset($usage['completion_tokens']) && !isset($usage['candidatesTokenCount']))) {
return null; return null;
} }
$cachedTokens = min($inputTokens, max(0, (int) ($usage['input_tokens_details']['cached_tokens'] ?? 0))); $cachedTokens = min($inputTokens, max(0, (int) ($usage['input_tokens_details']['cached_tokens'] ?? $usage['cachedContentTokenCount'] ?? 0)));
$cost = null; $cost = null;
$costSource = ''; $costSource = '';
@@ -159,7 +177,7 @@ class AiService
'model' => $model, 'model' => $model,
'input_tokens' => $inputTokens, 'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens, 'output_tokens' => $outputTokens,
'total_tokens' => (int) ($usage['total_tokens'] ?? ($inputTokens + $outputTokens)), 'total_tokens' => (int) ($usage['total_tokens'] ?? $usage['totalTokenCount'] ?? ($inputTokens + $outputTokens)),
'cached_input_tokens' => $cachedTokens, 'cached_input_tokens' => $cachedTokens,
'cost_usd' => $cost, 'cost_usd' => $cost,
'cost_source' => $costSource, 'cost_source' => $costSource,
@@ -179,6 +197,16 @@ class AiService
private function extractText(array $body): string private function extractText(array $body): string
{ {
if (isset($body['candidates'][0]['content']['parts']) && is_array($body['candidates'][0]['content']['parts'])) {
$parts = [];
foreach ($body['candidates'][0]['content']['parts'] as $part) {
if (isset($part['text']) && is_string($part['text'])) {
$parts[] = $part['text'];
}
}
return trim(implode("\n", $parts));
}
if (isset($body['choices'][0]['message']['content']) && is_string($body['choices'][0]['message']['content'])) { if (isset($body['choices'][0]['message']['content']) && is_string($body['choices'][0]['message']['content'])) {
return trim($body['choices'][0]['message']['content']); return trim($body['choices'][0]['message']['content']);
} }
@@ -29,6 +29,7 @@ class HtmlView extends BaseHtmlView
'openai' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0], 'openai' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0],
'grok' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0], 'grok' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0],
'groq' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0], 'groq' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0],
'gemini' => ['calls' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'total_tokens' => 0, 'cost_usd' => 0.0, 'cost_calls' => 0],
]; ];
$usageRows = $db->setQuery( $usageRows = $db->setQuery(
@@ -8,9 +8,11 @@
<?php $openaiKey = (string) ($this->settings['ai_openai_api_key'] ?? ($provider === 'openai' ? $legacyKey : '')); ?> <?php $openaiKey = (string) ($this->settings['ai_openai_api_key'] ?? ($provider === 'openai' ? $legacyKey : '')); ?>
<?php $grokKey = (string) ($this->settings['ai_grok_api_key'] ?? ($provider === 'grok' ? $legacyKey : '')); ?> <?php $grokKey = (string) ($this->settings['ai_grok_api_key'] ?? ($provider === 'grok' ? $legacyKey : '')); ?>
<?php $groqKey = (string) ($this->settings['ai_groq_api_key'] ?? ($provider === 'groq' ? $legacyKey : '')); ?> <?php $groqKey = (string) ($this->settings['ai_groq_api_key'] ?? ($provider === 'groq' ? $legacyKey : '')); ?>
<?php $geminiKey = (string) ($this->settings['ai_gemini_api_key'] ?? ($provider === 'gemini' ? $legacyKey : '')); ?>
<?php $openaiModel = (string) ($this->settings['ai_openai_model'] ?? 'gpt-4.1-mini'); ?> <?php $openaiModel = (string) ($this->settings['ai_openai_model'] ?? 'gpt-4.1-mini'); ?>
<?php $grokModel = (string) ($this->settings['ai_grok_model'] ?? 'grok-4.7'); ?> <?php $grokModel = (string) ($this->settings['ai_grok_model'] ?? 'grok-4.7'); ?>
<?php $groqModel = (string) ($this->settings['ai_groq_model'] ?? 'openai/gpt-oss-120b'); ?> <?php $groqModel = (string) ($this->settings['ai_groq_model'] ?? 'openai/gpt-oss-120b'); ?>
<?php $geminiModel = (string) ($this->settings['ai_gemini_model'] ?? 'gemini-3.8-flash'); ?>
<form action="<?php echo Route::_('index.php?option=com_testquestions&view=ai'); ?>" method="post" name="adminForm" id="adminForm"> <form action="<?php echo Route::_('index.php?option=com_testquestions&view=ai'); ?>" method="post" name="adminForm" id="adminForm">
<div class="card" style="max-width: 900px;"> <div class="card" style="max-width: 900px;">
<div class="card-body"> <div class="card-body">
@@ -20,6 +22,7 @@
<option value="openai" <?php echo $provider === 'openai' ? 'selected' : ''; ?>>OpenAI</option> <option value="openai" <?php echo $provider === 'openai' ? 'selected' : ''; ?>>OpenAI</option>
<option value="grok" <?php echo $provider === 'grok' ? 'selected' : ''; ?>>Grok</option> <option value="grok" <?php echo $provider === 'grok' ? 'selected' : ''; ?>>Grok</option>
<option value="groq" <?php echo $provider === 'groq' ? 'selected' : ''; ?>>Groq</option> <option value="groq" <?php echo $provider === 'groq' ? 'selected' : ''; ?>>Groq</option>
<option value="gemini" <?php echo $provider === 'gemini' ? 'selected' : ''; ?>>Gemini</option>
</select> </select>
</div> </div>
<div class="mb-3" data-ai-provider-section="openai"> <div class="mb-3" data-ai-provider-section="openai">
@@ -76,6 +79,24 @@
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="mb-3" data-ai-provider-section="gemini">
<label for="ai-gemini-api-key" class="form-label"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_AI_GEMINI_KEY'); ?></label>
<div class="input-group">
<input type="password" id="ai-gemini-api-key" name="ai_gemini_api_key" value="<?php echo htmlspecialchars($geminiKey, ENT_QUOTES, 'UTF-8'); ?>" class="form-control" size="120" maxlength="4096" autocomplete="new-password">
<button type="button" class="btn btn-outline-secondary" data-ai-key-toggle="ai-gemini-api-key">Pokaż</button>
</div>
<small class="form-text text-muted">
<?php echo $geminiKey !== '' ? 'Zapisany klucz: ' . mb_strlen($geminiKey) . ' znaków. Pozostaw pole bez zmian, aby go zachować.' : 'Nie zapisano klucza Gemini.'; ?>
</small>
</div>
<div class="mb-3" data-ai-provider-section="gemini">
<label for="ai-gemini-model" class="form-label"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_AI_GEMINI_MODEL'); ?></label>
<select id="ai-gemini-model" name="ai_gemini_model" class="form-select" required>
<?php foreach ($availableModels['gemini'] as $modelId => $modelLabel) : ?>
<option value="<?php echo htmlspecialchars($modelId, ENT_QUOTES, 'UTF-8'); ?>" <?php echo $geminiModel === $modelId ? 'selected' : ''; ?>><?php echo htmlspecialchars($modelLabel, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</div>
<hr> <hr>
<h3><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_AI_TOKEN_USAGE'); ?></h3> <h3><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_AI_TOKEN_USAGE'); ?></h3>
<p class="text-muted"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_AI_TOKEN_USAGE_INFO'); ?></p> <p class="text-muted"><?php echo \Joomla\CMS\Language\Text::_('COM_TESTQUESTIONS_AI_TOKEN_USAGE_INFO'); ?></p>
@@ -92,7 +113,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach (['openai' => 'OpenAI', 'grok' => 'Grok', 'groq' => 'Groq'] as $usageProvider => $usageProviderLabel) : ?> <?php foreach (['openai' => 'OpenAI', 'grok' => 'Grok', 'groq' => 'Groq', 'gemini' => 'Gemini'] as $usageProvider => $usageProviderLabel) : ?>
<?php $providerUsage = $usageTotals[$usageProvider]; ?> <?php $providerUsage = $usageTotals[$usageProvider]; ?>
<tr> <tr>
<th><?php echo $usageProviderLabel; ?></th> <th><?php echo $usageProviderLabel; ?></th>
@@ -64,7 +64,7 @@ $resolveImageUrl = static function ($path) {
} }
?> ?>
<?php if ($usageTotals['total_tokens'] > 0) : ?> <?php if ($usageTotals['total_tokens'] > 0) : ?>
<?php $usageProviderLabels = array_map(static function ($usageProvider) { return ['openai' => 'OpenAI', 'grok' => 'Grok', 'groq' => 'Groq'][$usageProvider] ?? $usageProvider; }, array_keys($usageTotals['providers'])); ?> <?php $usageProviderLabels = array_map(static function ($usageProvider) { return ['openai' => 'OpenAI', 'grok' => 'Grok', 'groq' => 'Groq', 'gemini' => 'Gemini'][$usageProvider] ?? $usageProvider; }, array_keys($usageTotals['providers'])); ?>
<span class="badge text-bg-light border ms-2" title="Suma dotychczasowych ocen AI"> <span class="badge text-bg-light border ms-2" title="Suma dotychczasowych ocen AI">
✦ <?php echo implode(' + ', array_map('htmlspecialchars', $usageProviderLabels)); ?>: ✦ <?php echo implode(' + ', array_map('htmlspecialchars', $usageProviderLabels)); ?>:
<?php echo number_format($usageTotals['total_tokens'], 0, ',', ' '); ?> tokenów, <?php echo number_format($usageTotals['total_tokens'], 0, ',', ' '); ?> tokenów,
@@ -96,8 +96,8 @@ $resolveImageUrl = static function ($path) {
<?php if (!empty($answer['ai_usage']) && is_array($answer['ai_usage'])) : ?> <?php if (!empty($answer['ai_usage']) && is_array($answer['ai_usage'])) : ?>
<?php $usage = $answer['ai_usage']; ?> <?php $usage = $answer['ai_usage']; ?>
<?php $usageProvider = (string) ($usage['provider'] ?? ''); ?> <?php $usageProvider = (string) ($usage['provider'] ?? ''); ?>
<span class="badge text-bg-light border ms-2" title="<?php echo $usageProvider === 'grok' ? 'Koszt zwrócony przez dostawcę' : ($usageProvider === 'groq' ? 'Groq zwrócił wykorzystane tokeny; koszt nie jest zwracany przez to API' : 'Koszt obliczony z faktycznie wykorzystanych tokenów i stawek modelu'); ?>"> <span class="badge text-bg-light border ms-2" title="<?php echo $usageProvider === 'grok' ? 'Koszt zwrócony przez dostawcę' : (in_array($usageProvider, ['groq', 'gemini'], true) ? 'Dostawca zwrócił wykorzystane tokeny; koszt nie jest zwracany przez to API' : 'Koszt obliczony z faktycznie wykorzystanych tokenów i stawek modelu'); ?>">
<?php echo $usageProvider === 'grok' ? '✦ Grok' : ($usageProvider === 'groq' ? '⚡ Groq' : '◉ OpenAI'); ?> · <?php echo $usageProvider === 'grok' ? '✦ Grok' : ($usageProvider === 'groq' ? '⚡ Groq' : ($usageProvider === 'gemini' ? '✧ Gemini' : '◉ OpenAI')); ?> ·
<?php echo number_format((int) ($usage['input_tokens'] ?? 0), 0, ',', ' '); ?> in / <?php echo number_format((int) ($usage['input_tokens'] ?? 0), 0, ',', ' '); ?> in /
<?php echo number_format((int) ($usage['output_tokens'] ?? 0), 0, ',', ' '); ?> out · <?php echo number_format((int) ($usage['output_tokens'] ?? 0), 0, ',', ' '); ?> out ·
<?php echo is_numeric($usage['cost_usd'] ?? null) ? number_format((float) $usage['cost_usd'], 8, ',', ' ') . ' USD' : 'koszt niedostępny'; ?> <?php echo is_numeric($usage['cost_usd'] ?? null) ? number_format((float) $usage['cost_usd'], 8, ',', ' ') . ' USD' : 'koszt niedostępny'; ?>
@@ -56,6 +56,12 @@ HTMLHelper::_('behavior.multiselect');
(<?php echo number_format((int) $this->aiUsageTotals['providers']['groq']['input_tokens'], 0, ',', ' '); ?> wej. / (<?php echo number_format((int) $this->aiUsageTotals['providers']['groq']['input_tokens'], 0, ',', ' '); ?> wej. /
<?php echo number_format((int) $this->aiUsageTotals['providers']['groq']['output_tokens'], 0, ',', ' '); ?> wyj.) <?php echo number_format((int) $this->aiUsageTotals['providers']['groq']['output_tokens'], 0, ',', ' '); ?> wyj.)
</span> </span>
<span class="d-block">
<strong>Gemini:</strong>
<?php echo number_format((int) $this->aiUsageTotals['providers']['gemini']['total_tokens'], 0, ',', ' '); ?> tokenów
(<?php echo number_format((int) $this->aiUsageTotals['providers']['gemini']['input_tokens'], 0, ',', ' '); ?> wej. /
<?php echo number_format((int) $this->aiUsageTotals['providers']['gemini']['output_tokens'], 0, ',', ' '); ?> wyj.)
</span>
<span class="d-block"><strong>Łącznie:</strong> <?php echo number_format((int) $this->aiUsageTotals['total_tokens'], 0, ',', ' '); ?> tokenów</span> <span class="d-block"><strong>Łącznie:</strong> <?php echo number_format((int) $this->aiUsageTotals['total_tokens'], 0, ',', ' '); ?> tokenów</span>
</div> </div>
<table class="table table-striped"> <table class="table table-striped">
@@ -92,6 +98,7 @@ HTMLHelper::_('behavior.multiselect');
OpenAI: <?php echo number_format((int) $item->ai_tokens_by_provider['openai'], 0, ',', ' '); ?> · OpenAI: <?php echo number_format((int) $item->ai_tokens_by_provider['openai'], 0, ',', ' '); ?> ·
Grok: <?php echo number_format((int) $item->ai_tokens_by_provider['grok'], 0, ',', ' '); ?> · Grok: <?php echo number_format((int) $item->ai_tokens_by_provider['grok'], 0, ',', ' '); ?> ·
Groq: <?php echo number_format((int) $item->ai_tokens_by_provider['groq'], 0, ',', ' '); ?> Groq: <?php echo number_format((int) $item->ai_tokens_by_provider['groq'], 0, ',', ' '); ?>
· Gemini: <?php echo number_format((int) $item->ai_tokens_by_provider['gemini'], 0, ',', ' '); ?>
</small> </small>
<small class="d-block text-muted"> <small class="d-block text-muted">
<?php echo number_format((int) $item->ai_total_tokens, 0, ',', ' '); ?> łącznie <?php echo number_format((int) $item->ai_total_tokens, 0, ',', ' '); ?> łącznie
+1 -1
View File
@@ -4,7 +4,7 @@
<creationDate>2026-10-14</creationDate> <creationDate>2026-10-14</creationDate>
<author>Komponent</author> <author>Komponent</author>
<license>GNU General Public License version 2 or later</license> <license>GNU General Public License version 2 or later</license>
<version>1.43.27</version> <version>1.43.28</version>
<description>COM_TESTQUESTIONS_DESCRIPTION</description> <description>COM_TESTQUESTIONS_DESCRIPTION</description>
<element>com_testquestions</element> <element>com_testquestions</element>
<namespace path="src">Komponent\Component\Testquestions</namespace> <namespace path="src">Komponent\Component\Testquestions</namespace>