optimizations
This commit is contained in:
210
app/Services/NovaCards/NovaCardAiAssistService.php
Normal file
210
app/Services/NovaCards/NovaCardAiAssistService.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\NovaCards;
|
||||
|
||||
use App\Models\NovaCard;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* AI-assist hooks for Nova Cards v3.
|
||||
*
|
||||
* This service provides assistive suggestions — it does NOT auto-publish
|
||||
* or override the creator. All suggestions are optional and editable.
|
||||
*
|
||||
* Integration strategy: each suggest* method returns a plain array of
|
||||
* suggestions. If an AI/ML backend is configured (via config/nova_cards.php),
|
||||
* this service dispatches to it; otherwise it uses deterministic rule-based
|
||||
* fallbacks. This keeps the system functional without a live AI dependency.
|
||||
*/
|
||||
class NovaCardAiAssistService
|
||||
{
|
||||
public function suggestTags(NovaCard $card): array
|
||||
{
|
||||
$text = implode(' ', array_filter([
|
||||
$card->quote_text,
|
||||
$card->quote_author,
|
||||
$card->title,
|
||||
$card->description,
|
||||
]));
|
||||
|
||||
// Rule-based: extract meaningful words as tag suggestions.
|
||||
// In production, replace/extend with an actual NLP/AI call.
|
||||
$words = preg_split('/[\s\-_,\.]+/u', strtolower($text));
|
||||
$stopWords = ['the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'or', 'is', 'it', 'be', 'by', 'that', 'this', 'with', 'you', 'your', 'we', 'our', 'not'];
|
||||
$filtered = array_values(array_filter(
|
||||
array_unique($words ?? []),
|
||||
fn ($w) => is_string($w) && mb_strlen($w) >= 4 && ! in_array($w, $stopWords, true),
|
||||
));
|
||||
|
||||
return array_slice($filtered, 0, 6);
|
||||
}
|
||||
|
||||
public function suggestMood(NovaCard $card): array
|
||||
{
|
||||
$text = strtolower((string) $card->quote_text . ' ' . (string) $card->title);
|
||||
$moods = [];
|
||||
|
||||
$moodMap = [
|
||||
'love|heart|romance|kiss|tender' => 'romantic',
|
||||
'dark|shadow|night|alone|silence|void|lost' => 'dark-poetry',
|
||||
'inspire|hope|dream|believe|courage|strength|rise' => 'inspirational',
|
||||
'morning|sunrise|calm|peace|gentle|soft|breeze' => 'soft-morning',
|
||||
'minimal|simple|quiet|still|breath|moment' => 'minimal',
|
||||
'power|bold|fierce|fire|warrior|fight|hustle' => 'motivational',
|
||||
'cyber|neon|digital|code|matrix|tech|signal' => 'cyber-mood',
|
||||
];
|
||||
|
||||
foreach ($moodMap as $pattern => $mood) {
|
||||
if (preg_match('/(' . $pattern . ')/i', $text)) {
|
||||
$moods[] = $mood;
|
||||
}
|
||||
}
|
||||
|
||||
return array_slice(array_unique($moods), 0, 3);
|
||||
}
|
||||
|
||||
public function suggestLayout(NovaCard $card): array
|
||||
{
|
||||
$project = is_array($card->project_json) ? $card->project_json : [];
|
||||
$quoteLength = mb_strlen((string) $card->quote_text);
|
||||
$hasAuthor = ! empty($card->quote_author);
|
||||
|
||||
// Heuristic layout suggestions based on text content.
|
||||
$suggestions = [];
|
||||
|
||||
if ($quoteLength < 80) {
|
||||
$suggestions[] = [
|
||||
'layout' => 'quote_centered',
|
||||
'reason' => 'Short quotes work well centered with generous padding.',
|
||||
];
|
||||
} elseif ($quoteLength < 200) {
|
||||
$suggestions[] = [
|
||||
'layout' => 'quote_heavy',
|
||||
'reason' => 'Medium quotes benefit from a focused heavy layout.',
|
||||
];
|
||||
} else {
|
||||
$suggestions[] = [
|
||||
'layout' => 'editorial',
|
||||
'reason' => 'Longer quotes fit editorial multi-column layouts.',
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasAuthor) {
|
||||
$suggestions[] = [
|
||||
'layout' => 'byline_bottom',
|
||||
'reason' => 'With an author, a bottom byline anchors attribution cleanly.',
|
||||
];
|
||||
}
|
||||
|
||||
return $suggestions;
|
||||
}
|
||||
|
||||
public function suggestBackground(NovaCard $card): array
|
||||
{
|
||||
$moods = $this->suggestMood($card);
|
||||
$suggestions = [];
|
||||
|
||||
$moodGradientMap = [
|
||||
'romantic' => ['gradient_preset' => 'rose-poetry', 'reason' => 'Warm rose tones suit romantic content.'],
|
||||
'dark-poetry' => ['gradient_preset' => 'midnight-nova', 'reason' => 'Deep dark gradients amplify dark poetry vibes.'],
|
||||
'inspirational' => ['gradient_preset' => 'golden-hour', 'reason' => 'Warm golden tones elevate inspirational messages.'],
|
||||
'soft-morning' => ['gradient_preset' => 'soft-dawn', 'reason' => 'Gentle pastels suit morning or calm content.'],
|
||||
'minimal' => ['gradient_preset' => 'carbon-minimal', 'reason' => 'Clean dark or light neutrals suit minimal style.'],
|
||||
'motivational' => ['gradient_preset' => 'bold-fire', 'reason' => 'Bold warm gradients energise motivational content.'],
|
||||
'cyber-mood' => ['gradient_preset' => 'cyber-pulse', 'reason' => 'Electric neon gradients suit cyber aesthetic.'],
|
||||
];
|
||||
|
||||
foreach ($moods as $mood) {
|
||||
if (isset($moodGradientMap[$mood])) {
|
||||
$suggestions[] = $moodGradientMap[$mood];
|
||||
}
|
||||
}
|
||||
|
||||
// Default if no match.
|
||||
if (empty($suggestions)) {
|
||||
$suggestions[] = ['gradient_preset' => 'midnight-nova', 'reason' => 'A versatile dark gradient that works for most content.'];
|
||||
}
|
||||
|
||||
return $suggestions;
|
||||
}
|
||||
|
||||
public function suggestFontPairing(NovaCard $card): array
|
||||
{
|
||||
$moods = $this->suggestMood($card);
|
||||
|
||||
$moodFontMap = [
|
||||
'romantic' => ['font_preset' => 'romantic-serif', 'reason' => 'Elegant serif pairs beautifully with romantic content.'],
|
||||
'dark-poetry' => ['font_preset' => 'dark-poetic', 'reason' => 'Strong contrast serif pairs with dark poetry style.'],
|
||||
'inspirational' => ['font_preset' => 'modern-sans', 'reason' => 'Clean modern sans feels energising and clear.'],
|
||||
'soft-morning' => ['font_preset' => 'soft-rounded', 'reason' => 'Rounded type has warmth and approachability.'],
|
||||
'minimal' => ['font_preset' => 'minimal-mono', 'reason' => 'Monospaced type enforces a minimalist aesthetic.'],
|
||||
'cyber-mood' => ['font_preset' => 'cyber-display', 'reason' => 'Tech display fonts suit cyber and digital themes.'],
|
||||
];
|
||||
|
||||
foreach ($moods as $mood) {
|
||||
if (isset($moodFontMap[$mood])) {
|
||||
return [$moodFontMap[$mood]];
|
||||
}
|
||||
}
|
||||
|
||||
return [['font_preset' => 'modern-sans', 'reason' => 'A clean, versatile sans-serif for most content.']];
|
||||
}
|
||||
|
||||
public function suggestReadabilityFixes(NovaCard $card): array
|
||||
{
|
||||
$issues = [];
|
||||
$project = is_array($card->project_json) ? $card->project_json : [];
|
||||
|
||||
$textColor = Arr::get($project, 'typography.text_color', '#ffffff');
|
||||
$bgType = Arr::get($project, 'background.type', 'gradient');
|
||||
$overlayStyle = Arr::get($project, 'background.overlay_style', 'dark-soft');
|
||||
$quoteSize = (int) Arr::get($project, 'typography.quote_size', 72);
|
||||
$lineHeight = (float) Arr::get($project, 'typography.line_height', 1.2);
|
||||
|
||||
// Detect light text without overlay on upload background.
|
||||
if ($bgType === 'upload' && in_array($overlayStyle, ['none', 'minimal'], true)) {
|
||||
$issues[] = [
|
||||
'field' => 'background.overlay_style',
|
||||
'message' => 'Adding a dark overlay improves text legibility on photo backgrounds.',
|
||||
'suggestion' => 'dark-soft',
|
||||
];
|
||||
}
|
||||
|
||||
// Detect very small quote text.
|
||||
if ($quoteSize < 40) {
|
||||
$issues[] = [
|
||||
'field' => 'typography.quote_size',
|
||||
'message' => 'Quote text may be too small to read on mobile.',
|
||||
'suggestion' => 52,
|
||||
];
|
||||
}
|
||||
|
||||
// Detect very tight line height on long text.
|
||||
if ($lineHeight < 1.1 && mb_strlen((string) $card->quote_text) > 100) {
|
||||
$issues[] = [
|
||||
'field' => 'typography.line_height',
|
||||
'message' => 'Increasing line height improves readability for longer quotes.',
|
||||
'suggestion' => 1.3,
|
||||
];
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all suggestions in one call, for the AI assist panel.
|
||||
*/
|
||||
public function allSuggestions(NovaCard $card): array
|
||||
{
|
||||
return [
|
||||
'tags' => $this->suggestTags($card),
|
||||
'moods' => $this->suggestMood($card),
|
||||
'layouts' => $this->suggestLayout($card),
|
||||
'backgrounds' => $this->suggestBackground($card),
|
||||
'font_pairings' => $this->suggestFontPairing($card),
|
||||
'readability_fixes' => $this->suggestReadabilityFixes($card),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user