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