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
50 changes: 11 additions & 39 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,19 @@

All notable changes to this project will be documented in this file.

## [1.1.0] - 2025-12-24
## [1.2.0] - 2026-02-18

### Added
- Public changelog page at /changelog displaying CHANGELOG.md with homepage design
- ChangelogController for parsing and displaying changelog entries
- config/app_config.php for centralized version management
- Changelog navigation link in main site header
- Version badge showing current version and release date
- Color-coded changelog sections (Added, Changed, Fixed, Removed)
- Card-based changelog design with card suit symbols
- AI provider configuration page for supervisors to manage API credentials, model selection, and generation parameters
- AI usage log for supervisors showing request history, token consumption, and estimated costs
- Secure credential storage and connection testing for AI service integration

### Changed
- Email templates now use Laravel mail layout with Cards Forge custom theme
- Email Templates and Scheduled Emails moved to System Settings navigation group
- Preview modal now displays emails with actual site header and footer styling
- Renamed "Run Now" to "Force Run" action with enhanced feedback showing sent/skipped counts
- Force Run action now explicitly shows scheduled time being bypassed in confirmation modal
## [1.1.0] - 2025-12-24

### Added
- Custom Email Template System for managing email templates with Markdown support
- EmailTemplate model and database table for storing reusable email templates
- EmailTemplateResource in Filament admin panel (supervisor-only access)
- Markdown editor with live preview functionality for email content
- Variable injection system supporting {{ variable }} placeholders in subject and body
- TemplateEmail mailable class for sending template-based emails with custom HTML layout
- Preview modal for templates showing rendered content
- Available variables cheat sheet in template form
- Custom HTML email template with responsive design and inline CSS
- Feature documentation in dps/features/custom-email-templates.md
- Advanced Scheduled Email Dispatcher for automated email campaigns
- ScheduledEmail model with cron expression scheduling and multi-source data support
- EmailDispatchLog model for deduplication tracking
- Cron expression validation with human-friendly display
- Dynamic recipient targeting (all users, specific roles, individual selection)
- Order status filtering with configurable look-back windows
- ScheduledEmailResource with reactive UI based on data source
- ProcessScheduledEmails command for email dispatch with deduplication
- Automatic scheduling via Laravel task scheduler (runs every minute)
- Force Run action with detailed execution feedback (sent/skipped counts)
- Bulk Force Run action to execute multiple campaigns simultaneously
- Clear Dispatch Logs header action for resetting deduplication during testing
- Comprehensive execution statistics tracking
- Enhanced testing capabilities with immediate execution bypassing schedules
- Feature documentation in dps/features/advanced-scheduled-email-dispatcher.md
- Custom email template system with variable injection and live preview for supervisors
- Scheduled email dispatcher with flexible recipient targeting and automated delivery
- Public changelog page accessible from the site navigation

### Changed
- Email notifications updated to use the Cards Forge branded layout
273 changes: 273 additions & 0 deletions app/Filament/Pages/AiSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<?php
/**
* Webtech-solutions 2025, All rights reserved.
*/

namespace App\Filament\Pages;

use App\Models\AiSetting;
use App\Services\Ai\AiProxyService;
use Filament\Actions\Action;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Pages\Page;

class AiSettings extends Page implements HasForms
{
use InteractsWithForms;

protected static ?string $navigationIcon = 'heroicon-o-cpu-chip';

protected static ?string $navigationLabel = 'AI Settings';

protected static ?string $navigationGroup = 'AI Management';

protected static ?int $navigationSort = 1;

protected static string $view = 'filament.pages.ai-settings';

public ?array $data = [];

/**
* Only supervisors can access this page.
*/
public static function canAccess(): bool
{
return auth()->check() && auth()->user()->isSupervisor();
}

/**
* Load current settings into the form.
*/
public function mount(): void
{
$this->form->fill([
'ai_provider' => AiSetting::get('ai_provider', 'openai'),
'ai_api_key' => AiSetting::get('ai_api_key', ''),
'ai_model' => AiSetting::get('ai_model', 'gpt-4o'),
'ai_max_tokens' => AiSetting::get('ai_max_tokens', 2048),
'ai_temperature' => AiSetting::get('ai_temperature', 0.7),
'ai_system_prompt' => AiSetting::get('ai_system_prompt', config('ai_prompts.card_generation.system', '')),
]);
}

/**
* Define the settings form.
*/
public function form(Form $form): Form
{
return $form
->schema([
Section::make('Provider Configuration')
->description('Configure the AI service provider and authentication.')
->icon('heroicon-o-cloud')
->schema([
Select::make('ai_provider')
->label('AI Provider')
->options([
'openai' => 'OpenAI',
'anthropic' => 'Anthropic (Claude)',
])
->required()
->live()
->afterStateUpdated(function ($state, $set) {
// Reset model when provider changes
$defaultModels = [
'openai' => 'gpt-4o',
'anthropic' => 'claude-sonnet-4-6',
];
$set('ai_model', $defaultModels[$state] ?? 'gpt-4o');
}),

TextInput::make('ai_api_key')
->label('API Key')
->password()
->revealable()
->placeholder('sk-... or sk-ant-...')
->helperText('Stored encrypted in the database. Leave blank to keep existing key.')
->maxLength(500),

Select::make('ai_model')
->label('Model')
->options(function ($get) {
$provider = $get('ai_provider') ?? 'openai';
return config("ai.providers.{$provider}.models", []);
})
->required()
->searchable(),
])
->columns(2),

Section::make('Generation Parameters')
->description('Control the behavior of AI-generated responses.')
->icon('heroicon-o-adjustments-horizontal')
->schema([
TextInput::make('ai_max_tokens')
->label('Max Tokens')
->numeric()
->minValue(100)
->maxValue(8192)
->default(2048)
->helperText('Maximum number of tokens in the AI response (100-8192).'),

TextInput::make('ai_temperature')
->label('Temperature')
->numeric()
->minValue(0)
->maxValue(2)
->step(0.1)
->default(0.7)
->helperText('Controls randomness: 0 = deterministic, 2 = very random. Recommended: 0.7'),
])
->columns(2),

Section::make('System Prompt')
->description('The default system prompt for card generation. Defines the AI\'s role and output format.')
->icon('heroicon-o-document-text')
->schema([
Textarea::make('ai_system_prompt')
->label('Card Generation System Prompt')
->rows(15)
->helperText('This prompt is sent with every card generation request. It defines the AI\'s domain knowledge and required JSON output format.')
->columnSpanFull(),
]),
])
->statePath('data');
}

/**
* Save the settings to the database.
*/
public function save(): void
{
$data = $this->form->getState();

// Only update API key if a new one was provided
if (!empty($data['ai_api_key'])) {
$this->saveEncryptedSetting('ai_api_key', $data['ai_api_key']);
}

$this->saveSetting('ai_provider', $data['ai_provider'], 'text', 'provider', 'AI Provider', 1);
$this->saveSetting('ai_model', $data['ai_model'], 'text', 'provider', 'AI Model', 2);
$this->saveSetting('ai_max_tokens', $data['ai_max_tokens'], 'number', 'parameters', 'Max Tokens', 1);
$this->saveSetting('ai_temperature', $data['ai_temperature'], 'number', 'parameters', 'Temperature', 2);
$this->saveSetting('ai_system_prompt', $data['ai_system_prompt'], 'textarea', 'prompts', 'System Prompt', 1);

Notification::make()
->title('Settings saved successfully.')
->success()
->send();
}

/**
* Test the current AI provider connection.
*/
public function testConnection(): void
{
// Save current form state first before testing
$data = $this->form->getState();
if (!empty($data['ai_api_key'])) {
$this->saveEncryptedSetting('ai_api_key', $data['ai_api_key']);
}
$this->saveSetting('ai_provider', $data['ai_provider'], 'text', 'provider', 'AI Provider', 1);
$this->saveSetting('ai_model', $data['ai_model'], 'text', 'provider', 'AI Model', 2);

try {
$service = new AiProxyService();
$result = $service->testConnection();

if ($result['success']) {
Notification::make()
->title('Connection successful!')
->body('The AI provider responded correctly.')
->success()
->send();
} elseif ($result['status'] === 'rate_limited') {
Notification::make()
->title('API key is valid (rate limited)')
->body($result['message'])
->warning()
->send();
} else {
Notification::make()
->title('Connection failed')
->body($result['message'])
->danger()
->send();
}
} catch (\Exception $e) {
Notification::make()
->title('Connection error')
->body($e->getMessage())
->danger()
->send();
}
}

/**
* Get the header actions (Save + Test Connection buttons).
*/
protected function getHeaderActions(): array
{
return [
Action::make('testConnection')
->label('Test Connection')
->icon('heroicon-o-signal')
->color('gray')
->action('testConnection'),

Action::make('save')
->label('Save Settings')
->icon('heroicon-o-check')
->color('primary')
->action('save'),
];
}

/**
* Save a plain-text setting.
*/
private function saveSetting(string $key, mixed $value, string $type, string $group, string $label, int $order): void
{
AiSetting::updateOrCreate(
['key' => $key],
[
'value' => $value,
'type' => $type,
'group' => $group,
'label' => $label,
'order' => $order,
]
);
}

/**
* Save an encrypted setting.
*/
private function saveEncryptedSetting(string $key, string $value): void
{
$existing = AiSetting::where('key', $key)->first();

if ($existing) {
// Directly update encrypted value using the mutator
$existing->type = 'encrypted';
$existing->value = $value;
$existing->save();
} else {
AiSetting::create([
'key' => $key,
'value' => $value,
'type' => 'encrypted',
'group' => 'provider',
'label' => 'API Key',
'order' => 3,
]);
}
}
}
Loading
Loading