Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ai_content_strategy.module
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function ai_content_strategy_theme() {
'categories' => [],
'button_text' => [],
'last_run' => NULL,
'ai_configured' => TRUE,
],
],
'ai_content_strategy_recommendations_items' => [
Expand Down
5 changes: 4 additions & 1 deletion scripts/run-drupal-lint-auto-fix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ phpcbf --standard=Drupal \
--extensions=php,module,inc,install,test,profile,theme,info,txt,md,yml \
--ignore=node_modules,ai_content_strategy/vendor,.github,vendor \
.
# phpcbf exit codes: 0=clean, 1=fixed, 2=unfixable, 3=error
# phpcbf exit codes: 0=no errors, 1=errors were fixed, 2=nothing to fix, 3=processing error
DRUPAL_STATUS=$?
if [ $DRUPAL_STATUS -eq 3 ]; then
exit 1
Expand All @@ -20,3 +20,6 @@ PRACTICE_STATUS=$?
if [ $PRACTICE_STATUS -eq 3 ]; then
exit 1
fi

# phpcbf exit 0 (clean) and 2 (nothing to fix) are both success.
exit 0
95 changes: 56 additions & 39 deletions src/Controller/ContentStrategyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,24 @@ public function recommendations() {
}
}

// Pre-flight check: is the AI provider configured?
$health_error = $this->strategyGenerator->checkHealth();
$ai_configured = $health_error === NULL;

if (!$ai_configured) {
$this->messenger()->addWarning($this->t('To generate content recommendations, first connect an AI service. <a href="@url">Open AI settings</a>', [
'@url' => '/admin/config/ai/settings',
]));
}

return [
'#theme' => 'ai_content_strategy_recommendations',
'#categories' => $categories,
'#last_run' => $last_run ?
$this->dateFormatter->formatTimeDiffSince($last_run) : NULL,
'#pages_analyzed' => $pages_analyzed,
'#categories_count' => count($category_ids),
'#ai_configured' => $ai_configured,
'#attached' => [
'library' => ['ai_content_strategy/content_strategy'],
],
Expand Down Expand Up @@ -1059,20 +1070,15 @@ public function addMoreRecommendations($section) {
catch (\Exception $e) {
Error::logException($this->getLogger('ai_content_strategy'), $e);

$error_message = $this->buildUserFriendlyErrorMessage($e);
$response->addCommand(
new MessageCommand(
$this->t(
'An error occurred while generating additional recommendations: @error',
[
'@error' => $e->getMessage(),
]),
$error_message,
NULL,
['type' => 'error']
)
);

// Set HTTP status code - use the exception's code if it's a valid HTTP
// status, otherwise default to 500.
$status_code = $this->getHttpStatusFromException($e);
$response->setStatusCode($status_code);
}
Expand Down Expand Up @@ -1118,58 +1124,69 @@ protected function replaceTokens($text, array $context) {
*/
protected function buildUserFriendlyErrorMessage(\Exception $exception): TranslatableMarkup {
$message = $exception->getMessage();
$lower = strtolower($message);

// Check for common error patterns and provide specific guidance.
// Pattern 1: No chat provider available.
if (stripos($message, 'no chat provider') !== FALSE || stripos($message, 'provider') !== FALSE) {
return $this->t('<strong>AI provider not configured.</strong><br>Please configure an AI chat provider at <a href="@url">AI settings</a> before generating recommendations.', [
'@url' => '/admin/config/ai/providers',
// AI provider not available or not configured.
if (str_contains($lower, 'no chat provider') || str_contains($lower, 'chat provider is not') || str_contains($lower, 'not properly configured') || str_contains($lower, 'no default chat model')) {
return $this->t('<strong>No AI service connected.</strong> To generate recommendations, first connect an AI chat provider. <a href="@url">Open AI settings</a>', [
'@url' => '/admin/config/ai/settings',
]);
}

// Pattern 2: No URLs found in sitemap.
if (stripos($message, 'no urls') !== FALSE || stripos($message, 'sitemap') !== FALSE) {
return $this->t('<strong>No content found to analyze.</strong><br>Please ensure your site has published content and a properly configured sitemap. Check your sitemap at <a href="@url">@url</a>.', [
'@url' => '/sitemap.xml',
// Provider or model not usable.
if (str_contains($lower, 'not usable') || str_contains($lower, 'not available for provider')) {
return $this->t('<strong>The selected AI model is not available.</strong> Your AI provider or model may have changed. Please check your configuration. <a href="@url">Open AI settings</a>', [
'@url' => '/admin/config/ai/settings',
]);
}

// Pattern 3: No enabled categories.
if (stripos($message, 'no enabled') !== FALSE || stripos($message, 'categor') !== FALSE) {
return $this->t('<strong>No recommendation categories enabled.</strong><br>Please enable at least one category at <a href="@url">category settings</a>.', [
// No sitemap / no content to analyze.
if (str_contains($lower, 'no urls found in sitemap') || str_contains($lower, 'could not analyze sitemap')) {
return $this->t('<strong>No content found to analyze.</strong> Make sure your site has published content and a working <a href="@sitemap">sitemap</a>. If you use the Simple Sitemap module, rebuild the sitemap first.', [
'@sitemap' => '/sitemap.xml',
]);
}

// No enabled recommendation categories.
if (str_contains($lower, 'no enabled recommendation categories')) {
return $this->t('<strong>No recommendation categories are turned on.</strong> Enable at least one category to generate recommendations. <a href="@url">Manage categories</a>', [
'@url' => '/admin/config/ai/content-strategy/categories',
]);
}

// Pattern 4: JSON parsing errors.
if (stripos($message, 'json') !== FALSE || stripos($message, 'parse') !== FALSE) {
return $this->t('<strong>AI returned an invalid response.</strong><br>The AI model may be overloaded or misconfigured. Please try again in a moment. If the problem persists, try a different AI model in <a href="@url">AI settings</a>.', [
'@url' => '/admin/config/ai/providers',
// AI returned unparseable response.
if (str_contains($lower, 'failed to parse') || str_contains($lower, 'invalid json') || str_contains($lower, 'invalid response format')) {
return $this->t('<strong>The AI returned an unusable response.</strong> This is usually temporary. Please try again. If it keeps happening, try a different AI model in <a href="@url">AI settings</a>.', [
'@url' => '/admin/config/ai/settings',
]);
}

// Pattern 5: API/Network errors.
if (stripos($message, 'timeout') !== FALSE || stripos($message, 'connection') !== FALSE || stripos($message, 'network') !== FALSE) {
return $this->t('<strong>Connection to AI provider failed.</strong><br>This may be a temporary network issue. Please check your internet connection and try again. If using a third-party API, verify your API credentials are correct.');
// Network / timeout errors.
if (str_contains($lower, 'timeout') || str_contains($lower, 'connection refused') || str_contains($lower, 'could not resolve host') || str_contains($lower, 'network is unreachable')) {
return $this->t('<strong>Could not reach the AI service.</strong> Check your internet connection and try again. If the problem persists, the AI service may be temporarily unavailable.');
}

// Pattern 6: Rate limiting.
if (stripos($message, 'rate limit') !== FALSE || stripos($message, 'quota') !== FALSE || stripos($message, 'too many') !== FALSE) {
return $this->t('<strong>AI service rate limit reached.</strong><br>You have exceeded the API usage limits. Please wait a few minutes before trying again, or check your API plan limits with your provider.');
// Rate limiting / quota.
if (str_contains($lower, 'rate limit') || str_contains($lower, 'quota exceeded') || str_contains($lower, 'too many requests') || str_contains($lower, '429')) {
return $this->t('<strong>AI request limit reached.</strong> Wait a few minutes and try again. If this happens frequently, check your API plan limits with your AI provider.');
}

// Pattern 7: Authentication errors.
if (stripos($message, 'auth') !== FALSE || stripos($message, 'api key') !== FALSE || stripos($message, 'credential') !== FALSE) {
return $this->t('<strong>AI provider authentication failed.</strong><br>Please verify your API credentials are correct at <a href="@url">AI settings</a>.', [
'@url' => '/admin/config/ai/providers',
// Authentication / API key errors.
if (str_contains($lower, 'authentication') || str_contains($lower, 'unauthorized') || str_contains($lower, 'invalid api key') || str_contains($lower, 'access denied') || str_contains($lower, '401') || str_contains($lower, '403')) {
return $this->t('<strong>AI service rejected your credentials.</strong> Check that your API key is correct and has not expired. <a href="@url">Open AI settings</a>', [
'@url' => '/admin/config/ai/settings',
]);
}

// Generic fallback with constructive guidance.
return $this->t('<strong>Unable to generate recommendations.</strong><br>An unexpected error occurred: @error<br><br><strong>What to try:</strong><ul><li>Refresh the page and try again</li><li>Check the <a href="@logs">error logs</a> for details</li><li>Verify your <a href="@ai">AI provider settings</a></li><li>Ensure your site has published content</li></ul>', [
// Card or idea not found (CRUD operations).
if (str_contains($lower, 'card not found') || str_contains($lower, 'idea not found')) {
return $this->t('<strong>Item not found.</strong> It may have already been deleted. Refresh the page to see the current state.');
}

// Generic fallback: state what happened, suggest a concrete next step.
return $this->t('<strong>Something went wrong.</strong> @error — <a href="@logs">Check the error log</a> for details, or try again.', [
'@error' => $message,
'@logs' => '/admin/reports/dblog',
'@ai' => '/admin/config/ai/providers',
]);
}

Expand Down Expand Up @@ -1317,7 +1334,7 @@ public function deleteCard(string $section, string $uuid) {

$response->addCommand(
new MessageCommand(
$this->t('Error deleting recommendation: @error', ['@error' => $e->getMessage()]),
$this->buildUserFriendlyErrorMessage($e),
NULL,
['type' => 'error']
)
Expand Down Expand Up @@ -1370,7 +1387,7 @@ public function deleteIdea(string $section, string $uuid, string $idea_uuid) {

$response->addCommand(
new MessageCommand(
$this->t('Error deleting content idea: @error', ['@error' => $e->getMessage()]),
$this->buildUserFriendlyErrorMessage($e),
NULL,
['type' => 'error']
)
Expand Down Expand Up @@ -1474,7 +1491,7 @@ public function saveCard(string $section, string $uuid) {

$response->addCommand(
new MessageCommand(
$this->t('Error saving: @error', ['@error' => $e->getMessage()]),
$this->buildUserFriendlyErrorMessage($e),
NULL,
['type' => 'error']
)
Expand Down
3 changes: 2 additions & 1 deletion templates/ai-content-strategy-recommendations.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* - pages_analyzed: Number of pages analyzed from the sitemap.
* - categories_count: Number of active recommendation categories.
* - configure_link: Link to configure categories (optional).
* - ai_configured: Whether the AI provider is configured and ready.
*
* @ingroup themeable
*/
Expand Down Expand Up @@ -44,7 +45,7 @@
{% endif %}

<div class="content-strategy-actions">
<button class="generate-recommendations button button--primary" data-has-existing="{{ last_run ? 'true' : 'false' }}">
<button class="generate-recommendations button button--primary" data-has-existing="{{ last_run ? 'true' : 'false' }}"{{ not ai_configured ? ' disabled' : '' }}>
{% if last_run %}
{{ 'Regenerate recommendations'|t }}
{% else %}
Expand Down