212 lines
7.8 KiB
PHP
212 lines
7.8 KiB
PHP
<?php
|
|
|
|
namespace Komponent\Component\Testquestions\Administrator\Model;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
use Joomla\CMS\Component\ComponentHelper;
|
|
use Komponent\Component\Testquestions\Administrator\Service\AiService;
|
|
|
|
class ResultModel extends BaseDatabaseModel
|
|
{
|
|
public function getItem($pk = null)
|
|
{
|
|
$id = (int) ($pk ?: Factory::getApplication()->getInput()->getInt('id'));
|
|
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
$item = $db->setQuery(
|
|
$db->getQuery(true)
|
|
->select(['r.*', 's.title AS set_title', 'sub.title AS subject_title'])
|
|
->from($db->quoteName('#__testquestions_results', 'r'))
|
|
->leftJoin($db->quoteName('#__testquestions_sets', 's') . ' ON s.id = r.set_id')
|
|
->leftJoin($db->quoteName('#__testquestions_subjects', 'sub') . ' ON sub.id = s.subject_id')
|
|
->where('r.id = ' . $id)
|
|
)->loadObject();
|
|
|
|
if (!$item) {
|
|
return null;
|
|
}
|
|
|
|
$item->answers = json_decode((string) $item->answers, true) ?: [];
|
|
$ids = array_map('intval', array_keys($item->answers));
|
|
$item->questions = [];
|
|
|
|
if ($ids) {
|
|
$item->questions = $db->setQuery(
|
|
$db->getQuery(true)
|
|
->select(['id', 'question_type', 'question', 'answer_a', 'answer_b', 'answer_c', 'answer_d', 'answer_open', 'correct_answer', 'points'])
|
|
->from($db->quoteName('#__testquestions_questions'))
|
|
->where('id IN (' . implode(',', $ids) . ')')
|
|
)->loadObjectList('id');
|
|
}
|
|
|
|
return $item;
|
|
}
|
|
|
|
public function saveAssessment(int $id, array $awardedPoints, array $comments): bool
|
|
{
|
|
$item = $this->getItem($id);
|
|
|
|
if (!$item) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($item->answers as $questionId => &$answer) {
|
|
$key = (string) $questionId;
|
|
$maxPoints = (int) ($item->questions[(int) $questionId]->points ?? 0);
|
|
$answer['awarded_points'] = min($maxPoints, max(0, (int) ($awardedPoints[$key] ?? 0)));
|
|
$answer['comment'] = trim((string) ($comments[$key] ?? ''));
|
|
}
|
|
unset($answer);
|
|
|
|
$correctionPoints = 0;
|
|
foreach ($item->answers as $answer) {
|
|
$correctionPoints += (int) ($answer['awarded_points'] ?? 0);
|
|
}
|
|
|
|
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
$row = (object) [
|
|
'id' => $id,
|
|
'answers' => json_encode($item->answers, JSON_UNESCAPED_UNICODE),
|
|
'points_after' => (int) $item->points_before + $correctionPoints,
|
|
];
|
|
|
|
return (bool) $db->updateObject('#__testquestions_results', $row, 'id');
|
|
}
|
|
|
|
public function evaluateWithAi(int $id, int $questionId): string
|
|
{
|
|
$item = $this->getItem($id);
|
|
|
|
if (!$item) {
|
|
throw new \RuntimeException('Nie znaleziono zapisanego testu.');
|
|
}
|
|
|
|
$answer = $item->answers[$questionId] ?? null;
|
|
$question = $item->questions[$questionId] ?? null;
|
|
|
|
if (!$item || !$answer || !$question || $question->question_type !== 'open' || !empty($answer['correct'])) {
|
|
throw new \RuntimeException('Nieprawidłowe pytanie do oceny AI.');
|
|
}
|
|
|
|
$params = ComponentHelper::getParams('com_testquestions');
|
|
$provider = trim((string) $params->get('ai_provider', 'openai'));
|
|
$apiKey = trim((string) $params->get('ai_api_key', ''));
|
|
$componentKeyLength = mb_strlen($apiKey);
|
|
|
|
// Odczytaj zapisane parametry bezpośrednio z bazy. Dzięki temu nie użyjemy
|
|
// nieaktualnej wartości z pamięci ComponentHelper, a maska pola hasła
|
|
// (np. ********) nie zostanie potraktowana jako prawdziwy klucz.
|
|
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
$storedParams = (string) $db->setQuery(
|
|
$db->getQuery(true)
|
|
->select($db->quoteName('params'))
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_testquestions'))
|
|
)->loadResult();
|
|
$storedParams = json_decode($storedParams, true);
|
|
|
|
if (is_array($storedParams)) {
|
|
$storedProvider = trim((string) ($storedParams['ai_provider'] ?? ''));
|
|
$storedApiKey = trim((string) ($storedParams['ai_api_key'] ?? ''));
|
|
|
|
if ($storedProvider !== '') {
|
|
$provider = $storedProvider;
|
|
}
|
|
|
|
if ($storedApiKey !== '' && !preg_match('/^\*+$/', $storedApiKey)) {
|
|
$apiKey = $storedApiKey;
|
|
}
|
|
}
|
|
|
|
if (trim($apiKey) === '') {
|
|
$databaseKey = is_array($storedParams) ? trim((string) ($storedParams['ai_api_key'] ?? '')) : '';
|
|
$databaseKeyLength = preg_match('/^\*+$/', $databaseKey) ? 0 : mb_strlen($databaseKey);
|
|
throw new \RuntimeException(
|
|
'AI_KEY_EMPTY|provider=' . ($provider !== '' ? $provider : '(pusty)')
|
|
. '|component_chars=' . $componentKeyLength
|
|
. '|database_chars=' . $databaseKeyLength
|
|
. '|database_params=' . (is_array($storedParams) ? 'json' : 'unreadable')
|
|
);
|
|
}
|
|
|
|
$aiResult = (new AiService())->evaluate(
|
|
$provider,
|
|
$apiKey,
|
|
strip_tags((string) $question->question),
|
|
(string) ($answer['selected'] ?? ''),
|
|
(string) $question->answer_open,
|
|
(int) $question->points
|
|
);
|
|
$item->answers[$questionId]['ai_feedback'] = $aiResult['feedback'];
|
|
$item->answers[$questionId]['awarded_points'] = $aiResult['awarded_points'];
|
|
|
|
$correctionPoints = 0;
|
|
foreach ($item->answers as $savedAnswer) {
|
|
$correctionPoints += (int) ($savedAnswer['awarded_points'] ?? 0);
|
|
}
|
|
|
|
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
$row = (object) [
|
|
'id' => $id,
|
|
'answers' => json_encode($item->answers, JSON_UNESCAPED_UNICODE),
|
|
'points_after' => (int) $item->points_before + $correctionPoints,
|
|
];
|
|
|
|
if (!$db->updateObject('#__testquestions_results', $row, 'id')) {
|
|
throw new \RuntimeException('Nie udało się zapisać oceny AI.');
|
|
}
|
|
|
|
return $aiResult['feedback'];
|
|
}
|
|
|
|
public function evaluateAllWithAi(int $id): array
|
|
{
|
|
$item = $this->getItem($id);
|
|
|
|
if (!$item) {
|
|
throw new \RuntimeException('Nie znaleziono zapisanego testu.');
|
|
}
|
|
|
|
$questionIds = [];
|
|
foreach ($item->answers as $questionId => $answer) {
|
|
$question = $item->questions[(int) $questionId] ?? null;
|
|
if ($question && $question->question_type === 'open' && empty($answer['correct'])) {
|
|
$questionIds[] = (int) $questionId;
|
|
}
|
|
}
|
|
|
|
if (!$questionIds) {
|
|
throw new \RuntimeException('AI_NO_OPEN_QUESTIONS');
|
|
}
|
|
|
|
$evaluated = 0;
|
|
$failed = 0;
|
|
$firstError = '';
|
|
|
|
foreach ($questionIds as $questionId) {
|
|
try {
|
|
$this->evaluateWithAi($id, $questionId);
|
|
$evaluated++;
|
|
} catch (\Throwable $error) {
|
|
$failed++;
|
|
$firstError = $firstError ?: $error->getMessage();
|
|
|
|
if (in_array($error->getMessage(), ['AI_NOT_CONFIGURED'], true)
|
|
|| str_starts_with($error->getMessage(), 'AI_KEY_EMPTY|')) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'evaluated' => $evaluated,
|
|
'failed' => $failed,
|
|
'first_error' => $firstError,
|
|
'total' => count($questionIds),
|
|
];
|
|
}
|
|
}
|