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), ]; } }