512 lines
19 KiB
PHP
Executable File
512 lines
19 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Enums\MessageType;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\InsightSchedule;
|
|
use App\Models\Subject;
|
|
use App\Models\UserSubject;
|
|
use App\Services\AIService;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\View\View;
|
|
|
|
/**
|
|
* Controller for managing user subjects and subscriptions.
|
|
*/
|
|
class SubjectController extends Controller
|
|
{
|
|
/**
|
|
* Display a listing of available subjects and user subscriptions.
|
|
*/
|
|
public function index(Request $request): View
|
|
{
|
|
$user = Auth::user();
|
|
|
|
/** @var \Illuminate\Database\Eloquent\Collection<int, Subject> $systemSubjects */
|
|
$systemSubjects = Subject::where('is_system', true)
|
|
->where('is_active', true)
|
|
->with(['userSubjects' => function ($query) use ($user) {
|
|
$query->where('user_id', $user->id);
|
|
}])
|
|
->get();
|
|
|
|
/** @var \Illuminate\Database\Eloquent\Collection<int, UserSubject> $mySubjects */
|
|
$mySubjects = UserSubject::where('user_id', $user->id)
|
|
->whereHas('subject', function ($query) {
|
|
$query->where('user_id', '!=', null)
|
|
->where('is_system', false);
|
|
})
|
|
->with('subject')
|
|
->get();
|
|
|
|
return view('admin.subjects.index', compact('systemSubjects', 'mySubjects'));
|
|
}
|
|
|
|
/**
|
|
* Store a newly created custom subject.
|
|
*/
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => ['required', 'string', 'max:255'],
|
|
'description' => ['nullable', 'string', 'max:1000'],
|
|
'introduction' => ['nullable', 'string', 'max:5000'],
|
|
'message_type' => ['nullable', 'string', 'max:50'],
|
|
'custom_instructions' => ['nullable', 'string', 'max:5000'],
|
|
]);
|
|
|
|
/** @var \App\Models\User $user */
|
|
$user = Auth::user();
|
|
|
|
/** @var Subject $subject */
|
|
$subject = Subject::create([
|
|
'user_id' => $user->id,
|
|
'name' => $validated['name'],
|
|
'description' => $validated['description'] ?? null,
|
|
'introduction' => $validated['introduction'] ?? null,
|
|
'message_type' => $validated['message_type'] ?? null,
|
|
'custom_instructions' => $validated['custom_instructions'] ?? null,
|
|
'is_system' => false,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// Auto-subscribe the user to their own subject
|
|
UserSubject::firstOrCreate(
|
|
[
|
|
'user_id' => $user->id,
|
|
'subject_id' => $subject->id,
|
|
],
|
|
[
|
|
'frequency' => 'daily',
|
|
'preferred_times' => ['09:00'],
|
|
'insight_language' => $user->locale ?? 'en',
|
|
'delivery_channels' => ['dashboard'],
|
|
'is_active' => true,
|
|
]
|
|
);
|
|
|
|
return back()->with('success', __('Subject created successfully.'));
|
|
}
|
|
|
|
/**
|
|
* Generate a new custom subject using AI.
|
|
*/
|
|
public function generateAiSubject(Request $request): RedirectResponse
|
|
{
|
|
$request->validate([
|
|
'theme' => ['required', 'string', 'max:255'],
|
|
]);
|
|
|
|
$theme = $request->input('theme');
|
|
|
|
$prompt = "Crie um assunto de aprendizado e treinamento sobre o tema: {$theme}. Retorne APENAS um objeto JSON válido (sem texto adicional, sem markdown, sem code blocks) com os campos: name (string, máximo 255 caracteres, formato: 'Aprendizado e Treinamento de [Tema]'), description (string, máximo 1000 caracteres), introduction (string, máximo 5000 caracteres). Todo o conteúdo deve estar em Português do Brasil. O assunto deve ser educativo, prático e adequado para geração de insights de aprendizagem pela IA.";
|
|
|
|
$ai = app(AIService::class);
|
|
$result = $ai->generateInsight($theme, $prompt, ['language' => 'pt_BR']);
|
|
|
|
if (!$result['success']) {
|
|
return back()->with('error', __('Failed to generate subject: :error', ['error' => $result['error'] ?? __('Unknown error')]));
|
|
}
|
|
|
|
$content = $result['content'];
|
|
$data = $this->extractJsonFromAiResponse($content);
|
|
|
|
if (!$data || !isset($data['name'])) {
|
|
$data = [
|
|
'name' => "Aprendizado e Treinamento de {$theme}",
|
|
'description' => $theme,
|
|
'introduction' => $content,
|
|
];
|
|
}
|
|
|
|
$user = Auth::user();
|
|
|
|
$subject = Subject::create([
|
|
'user_id' => $user->id,
|
|
'name' => $data['name'],
|
|
'description' => $data['description'] ?? null,
|
|
'introduction' => $data['introduction'] ?? null,
|
|
'is_system' => false,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
UserSubject::firstOrCreate(
|
|
[
|
|
'user_id' => $user->id,
|
|
'subject_id' => $subject->id,
|
|
],
|
|
[
|
|
'frequency' => 'daily',
|
|
'preferred_times' => ['09:00'],
|
|
'insight_language' => $user->locale ?? 'en',
|
|
'delivery_channels' => ['dashboard'],
|
|
'is_active' => true,
|
|
]
|
|
);
|
|
|
|
return back()->with('success', __('Subject created successfully.'));
|
|
}
|
|
|
|
/**
|
|
* Extract JSON from AI response, handling code blocks and plain text.
|
|
*/
|
|
private function extractJsonFromAiResponse(string $content): ?array
|
|
{
|
|
$content = trim($content);
|
|
|
|
if (str_starts_with($content, '```')) {
|
|
$content = preg_replace('/^```(?:json)?\s*/', '', $content);
|
|
$content = preg_replace('/\s*```$/', '', $content);
|
|
}
|
|
|
|
$data = json_decode($content, true);
|
|
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
|
|
return $data;
|
|
}
|
|
|
|
if (preg_match('/\{.*\}/s', $content, $matches)) {
|
|
$data = json_decode($matches[0], true);
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
|
|
return $data;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Subscribe to a subject with scheduling preferences.
|
|
*/
|
|
public function subscribe(Request $request, Subject $subject): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'frequency' => ['required', 'string', 'in:daily,weekly,biweekly,monthly'],
|
|
'preferred_times' => ['required', 'array', 'min:1'],
|
|
'preferred_times.*' => ['required', 'string', 'date_format:H:i'],
|
|
'weekdays' => ['nullable', 'array'],
|
|
'weekdays.*' => ['integer', 'between:0,6'],
|
|
'insight_language' => ['required', 'string', 'in:pt_BR,en,es'],
|
|
'delivery_channels' => ['nullable', 'array'],
|
|
'delivery_channels.*' => ['string', 'in:dashboard,telegram'],
|
|
]);
|
|
|
|
/** @var \App\Models\User $user */
|
|
$user = Auth::user();
|
|
|
|
$channels = $validated['delivery_channels'] ?? ['dashboard'];
|
|
if (!in_array('dashboard', $channels, true)) {
|
|
$channels[] = 'dashboard'; // Dashboard is always enabled
|
|
}
|
|
|
|
UserSubject::updateOrCreate(
|
|
[
|
|
'user_id' => $user->id,
|
|
'subject_id' => $subject->id,
|
|
],
|
|
[
|
|
'frequency' => $validated['frequency'],
|
|
'preferred_times' => $validated['preferred_times'],
|
|
'weekdays' => $validated['weekdays'] ?? null,
|
|
'insight_language' => $validated['insight_language'],
|
|
'delivery_channels' => $channels,
|
|
'is_active' => true,
|
|
]
|
|
);
|
|
|
|
return back()->with('success', __('Subscribed to subject successfully.'));
|
|
}
|
|
|
|
/**
|
|
* Update the specified subject.
|
|
*/
|
|
public function update(Request $request, Subject $subject): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => ['required', 'string', 'max:255'],
|
|
'description' => ['nullable', 'string', 'max:1000'],
|
|
'message_type' => ['nullable', 'string', 'max:50'],
|
|
'custom_instructions' => ['nullable', 'string', 'max:5000'],
|
|
'is_active' => ['boolean'],
|
|
]);
|
|
|
|
$subject->update([
|
|
'name' => $validated['name'],
|
|
'description' => $validated['description'] ?? $subject->description,
|
|
'message_type' => $validated['message_type'] ?? $subject->message_type,
|
|
'custom_instructions' => $validated['custom_instructions'] ?? $subject->custom_instructions,
|
|
'is_active' => $validated['is_active'] ?? $subject->is_active,
|
|
]);
|
|
|
|
return back()->with('success', __('Subject updated successfully.'));
|
|
}
|
|
|
|
/**
|
|
* Toggle the active status of a user's subject subscription.
|
|
*/
|
|
public function toggle(Request $request, UserSubject $userSubject): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'is_active' => ['required', 'boolean'],
|
|
]);
|
|
|
|
$userSubject->update([
|
|
'is_active' => $validated['is_active'],
|
|
]);
|
|
|
|
if (!$validated['is_active']) {
|
|
InsightSchedule::where('user_id', $userSubject->user_id)
|
|
->where('subject_id', $userSubject->subject_id)
|
|
->delete();
|
|
}
|
|
|
|
$status = $validated['is_active'] ? __('activated') : __('deactivated');
|
|
|
|
return back()->with('success', __("Subject {$status} successfully."));
|
|
}
|
|
|
|
/**
|
|
* Update the schedule for a user's subject subscription.
|
|
*/
|
|
public function updateSchedule(Request $request, UserSubject $userSubject): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'frequency' => ['required', 'string', 'in:daily,weekly,biweekly,monthly'],
|
|
'preferred_times' => ['required', 'array', 'min:1'],
|
|
'preferred_times.*' => ['required', 'string', 'date_format:H:i'],
|
|
'weekdays' => ['nullable', 'array'],
|
|
'weekdays.*' => ['integer', 'between:0,6'],
|
|
'insight_language' => ['required', 'string', 'in:pt_BR,en,es'],
|
|
'delivery_channels' => ['nullable', 'array'],
|
|
'delivery_channels.*' => ['string', 'in:dashboard,telegram'],
|
|
]);
|
|
|
|
$channels = $validated['delivery_channels'] ?? ['dashboard'];
|
|
if (!in_array('dashboard', $channels, true)) {
|
|
$channels[] = 'dashboard';
|
|
}
|
|
|
|
$userSubject->update([
|
|
'frequency' => $validated['frequency'],
|
|
'preferred_times' => $validated['preferred_times'],
|
|
'weekdays' => $validated['weekdays'] ?? $userSubject->weekdays,
|
|
'insight_language' => $validated['insight_language'],
|
|
'delivery_channels' => $channels,
|
|
]);
|
|
|
|
return back()->with('success', __('Schedule updated successfully.'));
|
|
}
|
|
|
|
/**
|
|
* Remove the specified subject.
|
|
*/
|
|
public function destroy(Subject $subject): RedirectResponse
|
|
{
|
|
/** @var \App\Models\User $user */
|
|
$user = Auth::user();
|
|
|
|
// Only allow deletion if the user owns the subject or is SuperAdmin
|
|
if ($subject->user_id !== $user->id && !$user->hasRole('superadmin')) {
|
|
return back()->with('error', __('You do not have permission to delete this subject.'));
|
|
}
|
|
|
|
$subject->delete();
|
|
|
|
return back()->with('success', __('Subject deleted successfully.'));
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe from a subject.
|
|
*/
|
|
public function unsubscribe(UserSubject $userSubject): RedirectResponse
|
|
{
|
|
$userSubject->delete();
|
|
|
|
return back()->with('success', __('Unsubscribed from subject successfully.'));
|
|
}
|
|
|
|
/**
|
|
* Send an immediate AI insight for a subject.
|
|
*/
|
|
public function sendInsight(Request $request, Subject $subject): RedirectResponse
|
|
{
|
|
/** @var \App\Models\User $user */
|
|
$user = Auth::user();
|
|
|
|
$ai = app(AIService::class);
|
|
|
|
$messageType = MessageType::tryFrom($request->input('message_type', '')) ?? MessageType::AiGenerated;
|
|
|
|
if ($messageType === MessageType::AiGenerated && $subject->message_type) {
|
|
$messageType = MessageType::tryFrom($subject->message_type) ?? MessageType::AiGenerated;
|
|
}
|
|
|
|
// Get the user's language preference for this subject
|
|
$userSubject = UserSubject::where('user_id', $user->id)
|
|
->where('subject_id', $subject->id)
|
|
->first();
|
|
|
|
$language = $userSubject->insight_language ?? $user->locale ?? 'en';
|
|
|
|
$prompt = match ($messageType) {
|
|
MessageType::LanguageLesson => __('Teach me a practical language lesson about :subject with examples, practice sentences, pronunciation, grammar, and vocabulary.', ['subject' => $subject->name]),
|
|
MessageType::GameSimulator => __('Give me a strategic tip for :subject with actionable strategies, commands, and practical steps.', ['subject' => $subject->name]),
|
|
MessageType::TechnicalTeaching => __('Give me a technical teaching lesson about :subject covering components, techniques, repairs, schematics, and practical exercises.', ['subject' => $subject->name]),
|
|
default => __('Generate an insightful learning tip about :subject. Include practical advice and key concepts.', ['subject' => $subject->name]),
|
|
};
|
|
|
|
$context = ['language' => $language];
|
|
|
|
if ($subject->introduction) {
|
|
$context['introduction'] = $subject->introduction;
|
|
$prompt = __('Context about :subject: :introduction', [
|
|
'subject' => $subject->name,
|
|
'introduction' => $subject->introduction,
|
|
]) . "\n\n" . $prompt;
|
|
}
|
|
|
|
$result = $ai->generateInsight($subject->name, $prompt, $context, $messageType);
|
|
|
|
if (!$result['success']) {
|
|
return back()->with('error', __('Failed to generate message: :error', [
|
|
'error' => $result['error'] ?? __('Unknown error'),
|
|
]));
|
|
}
|
|
|
|
\App\Models\Insight::create([
|
|
'user_id' => $user->id,
|
|
'subject_id' => $subject->id,
|
|
'content' => $result['content'],
|
|
'type' => $messageType->value,
|
|
'input_tokens' => $result['input_tokens'],
|
|
'output_tokens' => $result['output_tokens'],
|
|
'sent_at' => now(),
|
|
]);
|
|
|
|
\App\Models\TokenUsage::create([
|
|
'user_id' => $user->id,
|
|
'input_tokens' => $result['input_tokens'],
|
|
'output_tokens' => $result['output_tokens'],
|
|
'total_tokens' => $result['input_tokens'] + $result['output_tokens'],
|
|
'model' => config('services.openrouter.model', 'google/gemini-2.0-flash-exp:free'),
|
|
'provider' => 'openrouter',
|
|
]);
|
|
|
|
// Send to Telegram if user has a bot configured
|
|
$this->sendToTelegram($user, $subject->id, $result['content']);
|
|
|
|
return back()->with('success', __('Message generated and sent successfully!'));
|
|
}
|
|
|
|
/**
|
|
* Send an AI-generated introduction for a subject as an insight.
|
|
* Always generates a fresh introduction via OpenRouter AI.
|
|
*/
|
|
public function sendIntroduction(Request $request, Subject $subject): RedirectResponse
|
|
{
|
|
/** @var \App\Models\User $user */
|
|
$user = Auth::user();
|
|
|
|
$ai = app(AIService::class);
|
|
|
|
$userSubject = UserSubject::where('user_id', $user->id)
|
|
->where('subject_id', $subject->id)
|
|
->first();
|
|
|
|
$language = $userSubject->insight_language ?? $user->locale ?? 'en';
|
|
|
|
$prompt = "Create a comprehensive and engaging introduction about the subject: {$subject->name}. "
|
|
. "Explain what it is, why it's important to learn, its key concepts, "
|
|
. "and what the learner will gain from studying it. "
|
|
. "Be informative, inspiring, and practical.";
|
|
|
|
$context = ['language' => $language];
|
|
|
|
if ($subject->introduction) {
|
|
$context['existing_introduction'] = $subject->introduction;
|
|
$prompt .= "\n\nHere is some context about the subject: {$subject->introduction}";
|
|
}
|
|
|
|
$result = $ai->generateInsight($subject->name, $prompt, $context, MessageType::Introduction);
|
|
|
|
if (!$result['success']) {
|
|
return back()->with('error', __('Failed to generate introduction: :error', [
|
|
'error' => $result['error'] ?? __('Unknown error'),
|
|
]));
|
|
}
|
|
|
|
$subject->update(['introduction' => $result['content']]);
|
|
|
|
\App\Models\Insight::create([
|
|
'user_id' => $user->id,
|
|
'subject_id' => $subject->id,
|
|
'content' => $result['content'],
|
|
'type' => MessageType::Introduction->value,
|
|
'input_tokens' => $result['input_tokens'],
|
|
'output_tokens' => $result['output_tokens'],
|
|
'sent_at' => now(),
|
|
]);
|
|
|
|
\App\Models\TokenUsage::create([
|
|
'user_id' => $user->id,
|
|
'input_tokens' => $result['input_tokens'],
|
|
'output_tokens' => $result['output_tokens'],
|
|
'total_tokens' => $result['input_tokens'] + $result['output_tokens'],
|
|
'model' => config('services.openrouter.model'),
|
|
'provider' => 'openrouter',
|
|
]);
|
|
|
|
// Send to Telegram if user has a bot configured
|
|
$this->sendToTelegram($user, $subject->id, $result['content']);
|
|
|
|
return back()->with('success', __('Introduction generated by AI and sent successfully!'));
|
|
}
|
|
|
|
/**
|
|
* Send insight content to the user's Telegram bot if configured and enabled.
|
|
*/
|
|
private function sendToTelegram(\App\Models\User $user, int $subjectId, string $content): void
|
|
{
|
|
try {
|
|
// Check if user has Telegram enabled for this subject
|
|
$userSubject = \App\Models\UserSubject::where('user_id', $user->id)
|
|
->where('subject_id', $subjectId)
|
|
->first();
|
|
|
|
// If no user subject found or telegram channel not enabled, skip
|
|
if (!$userSubject || !$userSubject->hasChannel('telegram')) {
|
|
return;
|
|
}
|
|
|
|
/** @var \App\Models\TelegramUser|null $telegramUser */
|
|
$telegramUser = $user->telegramUsers()->where('is_active', true)->first();
|
|
|
|
if (!$telegramUser || !$telegramUser->isFullyConfigured()) {
|
|
return;
|
|
}
|
|
|
|
$subject = \App\Models\Subject::find($subjectId);
|
|
$subjectName = $subject ? $subject->name : __('Subject');
|
|
|
|
$telegramService = new \App\Services\TelegramService($telegramUser->bot_token);
|
|
|
|
// Strip markdown for Telegram (it has its own formatting)
|
|
$plainText = \App\Services\MarkdownService::strip($content);
|
|
$message = "📚 *{$subjectName}*\n\n{$plainText}";
|
|
|
|
$telegramService->sendMessage($telegramUser->chat_id, $message);
|
|
} catch (\Exception $e) {
|
|
\Illuminate\Support\Facades\Log::error('Failed to send insight to Telegram', [
|
|
'user_id' => $user->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
}
|