First Commit
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\SubscriptionPlan;
|
||||
use App\Models\UserNotification;
|
||||
use App\Models\UserSubscription;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BillingController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
$activeSubscription = UserSubscription::where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->with('plan')
|
||||
->first();
|
||||
|
||||
$subscriptionHistory = UserSubscription::where('user_id', $user->id)
|
||||
->with('plan')
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
$invoices = Invoice::where('user_id', $user->id)
|
||||
->with('subscription.plan')
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
$plans = SubscriptionPlan::where('is_active', true)->get();
|
||||
|
||||
$notifications = UserNotification::where('user_id', $user->id)
|
||||
->where('is_read', false)
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
return view('admin.billing.index', compact(
|
||||
'activeSubscription', 'subscriptionHistory', 'invoices', 'plans', 'notifications'
|
||||
));
|
||||
}
|
||||
|
||||
public function subscribe(Request $request, SubscriptionPlan $plan): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Check if user already has active subscription
|
||||
$existing = UserSubscription::where('user_id', $user->id)
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return back()->with('error', __('You already have an active subscription.'));
|
||||
}
|
||||
|
||||
$subscription = UserSubscription::create([
|
||||
'user_id' => $user->id,
|
||||
'subscription_plan_id' => $plan->id,
|
||||
'start_date' => now(),
|
||||
'end_date' => now()->addDays($plan->duration_days),
|
||||
'status' => 'active',
|
||||
'payment_approved' => false,
|
||||
]);
|
||||
|
||||
// Create pending invoice
|
||||
$lastInvoice = Invoice::orderByDesc('id')->first();
|
||||
$nextNumber = $lastInvoice ? (int)filter_var($lastInvoice->invoice_number, FILTER_SANITIZE_NUMBER_INT) + 1 : 1001;
|
||||
|
||||
Invoice::create([
|
||||
'user_id' => $user->id,
|
||||
'user_subscription_id' => $subscription->id,
|
||||
'invoice_number' => 'INV-' . $nextNumber . '/' . now()->year,
|
||||
'amount' => $plan->price,
|
||||
'currency' => $plan->currency,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
// Notify SuperAdmin
|
||||
UserNotification::create([
|
||||
'user_id' => 1, // SuperAdmin
|
||||
'type' => 'admin_message',
|
||||
'title' => __('New Subscription'),
|
||||
'message' => __('User :name subscribed to plan :plan. Payment pending approval.', [
|
||||
'name' => $user->name,
|
||||
'plan' => $plan->name,
|
||||
]),
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Subscription created successfully. Your payment is pending approval.'));
|
||||
}
|
||||
|
||||
public function downloadInvoice(Invoice $invoice)
|
||||
{
|
||||
if ($invoice->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if ($invoice->pdf_path && Storage::exists($invoice->pdf_path)) {
|
||||
return Storage::download($invoice->pdf_path, "invoice-{$invoice->invoice_number}.pdf");
|
||||
}
|
||||
|
||||
return back()->with('error', __('Invoice PDF not available yet.'));
|
||||
}
|
||||
|
||||
public function notifications(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
$notifications = UserNotification::where('user_id', $user->id)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(20);
|
||||
|
||||
return view('admin.billing.notifications', compact('notifications'));
|
||||
}
|
||||
|
||||
public function markNotificationRead(UserNotification $notification): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
if ($notification->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$notification->markAsRead();
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function markAllNotificationsRead(): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
UserNotification::where('user_id', Auth::id())
|
||||
->where('is_read', false)
|
||||
->update(['is_read' => true, 'read_at' => now()]);
|
||||
|
||||
return back()->with('success', __('All notifications marked as read.'));
|
||||
}
|
||||
}
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CustomSubject;
|
||||
use App\Models\CustomSubjectQuestion;
|
||||
use App\Models\TokenUsage;
|
||||
use App\Services\AIService;
|
||||
use App\Services\PdfExtractionService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Controller for managing user custom subjects created from PDFs.
|
||||
*/
|
||||
class CustomSubjectController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the user's custom subjects.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$customSubjects = CustomSubject::where('user_id', $user->id)
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
return view('admin.custom-subjects.index', compact('customSubjects'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created custom subject from a PDF upload.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'pdf' => ['required', 'file', 'mimetypes:application/pdf', 'max:10240'],
|
||||
]);
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$pdfFile = $request->file('pdf');
|
||||
|
||||
if (!$pdfFile->isValid()) {
|
||||
return back()->with('error', __('Failed to upload PDF. Please try again.'));
|
||||
}
|
||||
|
||||
$fileName = time() . '_' . uniqid() . '.pdf';
|
||||
$storagePath = 'custom_subjects/' . $user->id;
|
||||
$pdfPath = $pdfFile->storeAs($storagePath, $fileName, 'local');
|
||||
|
||||
$customSubject = CustomSubject::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $validated['name'],
|
||||
'pdf_path' => $pdfPath,
|
||||
'processing_status' => 'processing',
|
||||
]);
|
||||
|
||||
// Extract text asynchronously (in real app, use a queue job)
|
||||
$pdfExtractionService = new PdfExtractionService();
|
||||
$fullPath = Storage::disk('local')->path($pdfPath);
|
||||
$result = $pdfExtractionService->extract($fullPath);
|
||||
|
||||
if ($result['success']) {
|
||||
$customSubject->update([
|
||||
'extracted_content' => $result['content'],
|
||||
'processing_status' => 'completed',
|
||||
]);
|
||||
} else {
|
||||
$customSubject->update([
|
||||
'processing_status' => 'failed',
|
||||
'error_message' => $result['error'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
return back()->with('success', __('Custom subject created successfully.'));
|
||||
}
|
||||
|
||||
return back()->with('error', __('Failed to process PDF: :error', ['error' => $result['error']]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified custom subject.
|
||||
*/
|
||||
public function edit(CustomSubject $customSubject): View
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($customSubject->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('admin.custom-subjects.edit', compact('customSubject'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified custom subject.
|
||||
*/
|
||||
public function update(Request $request, CustomSubject $customSubject): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($customSubject->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$customSubject->update([
|
||||
'name' => $validated['name'],
|
||||
'is_active' => $request->has('is_active'),
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Custom subject updated successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified custom subject with Q&A interface.
|
||||
*/
|
||||
public function show(CustomSubject $customSubject): View
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($customSubject->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$questions = $customSubject->questions()
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
// Get AI settings from DB (unified for all subjects), fallback to config
|
||||
$provider = \App\Models\Setting::where('key', 'ai_default_provider')->value('value');
|
||||
$model = \App\Models\Setting::where('key', 'ai_default_model')->value('value');
|
||||
|
||||
if (!$provider) {
|
||||
$provider = config('services.ai.default_provider', 'openrouter');
|
||||
}
|
||||
|
||||
if (!$model) {
|
||||
$model = config('services.ai.providers.' . $provider . '.model', 'inclusionai/ling-3.0-tiny:free');
|
||||
}
|
||||
|
||||
return view('admin.custom-subjects.show', compact('customSubject', 'questions', 'provider', 'model'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask a question about the custom subject and get an AI answer.
|
||||
*/
|
||||
public function ask(Request $request, CustomSubject $customSubject): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($customSubject->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if ($customSubject->processing_status !== 'completed' || empty($customSubject->extracted_content)) {
|
||||
return back()->with('error', __('This PDF is still being processed or has no content.'));
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'question' => ['required', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$question = trim($validated['question']);
|
||||
|
||||
// Get AI settings from DB (unified for all subjects), fallback to config
|
||||
$provider = \App\Models\Setting::where('key', 'ai_default_provider')->value('value');
|
||||
$model = \App\Models\Setting::where('key', 'ai_default_model')->value('value');
|
||||
|
||||
if (!$provider) {
|
||||
$provider = config('services.ai.default_provider', 'openrouter');
|
||||
}
|
||||
|
||||
if (!$model) {
|
||||
$model = config('services.ai.providers.' . $provider . '.model', 'inclusionai/ling-3.0-tiny:free');
|
||||
}
|
||||
|
||||
$language = $user->locale ?? 'en';
|
||||
$languageNames = [
|
||||
'pt_BR' => 'Brazilian Portuguese',
|
||||
'en' => 'English',
|
||||
'es' => 'Spanish',
|
||||
];
|
||||
$languageName = $languageNames[$language] ?? 'English';
|
||||
|
||||
$systemPrompt = "You are an expert assistant analyzing the content of a document titled '{$customSubject->name}'. "
|
||||
. "Your task is to answer questions based ONLY on the provided document content. "
|
||||
. "If the answer is not found in the document, say so clearly. "
|
||||
. "Do NOT make up information. "
|
||||
. "CRITICAL LANGUAGE INSTRUCTION: You MUST respond ONLY in {$languageName} ({$language}). "
|
||||
. "Do NOT use any other language.";
|
||||
|
||||
$userMessage = "Document content:\n\n{$customSubject->extracted_content}\n\n"
|
||||
. "Question: {$question}\n\n"
|
||||
. "CRITICAL: Answer EXCLUSIVELY in {$languageName}. Do NOT use English. Do NOT translate. "
|
||||
. "The entire response must be ONLY in {$languageName}.";
|
||||
|
||||
$messages = [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $userMessage],
|
||||
];
|
||||
|
||||
$aiService = new AIService($provider, $model);
|
||||
$result = $aiService->chat($messages);
|
||||
|
||||
if (!$result['success']) {
|
||||
return back()->with('error', __('Failed to get response: :error', [
|
||||
'error' => $result['error'] ?? __('Unknown error'),
|
||||
]));
|
||||
}
|
||||
|
||||
$answer = $result['content'];
|
||||
$inputTokens = $result['input_tokens'] ?? 0;
|
||||
$outputTokens = $result['output_tokens'] ?? 0;
|
||||
|
||||
CustomSubjectQuestion::create([
|
||||
'custom_subject_id' => $customSubject->id,
|
||||
'user_id' => $user->id,
|
||||
'question' => $question,
|
||||
'answer' => $answer,
|
||||
'provider' => $provider,
|
||||
'model' => $model,
|
||||
'input_tokens' => $inputTokens,
|
||||
'output_tokens' => $outputTokens,
|
||||
'asked_at' => now(),
|
||||
]);
|
||||
|
||||
// Track token usage
|
||||
if ($inputTokens > 0 || $outputTokens > 0) {
|
||||
TokenUsage::create([
|
||||
'user_id' => $user->id,
|
||||
'input_tokens' => $inputTokens,
|
||||
'output_tokens' => $outputTokens,
|
||||
'total_tokens' => $inputTokens + $outputTokens,
|
||||
'model' => $model,
|
||||
'provider' => $provider,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', __('Question answered successfully!'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified custom subject.
|
||||
*/
|
||||
public function destroy(CustomSubject $customSubject): RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($customSubject->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Delete PDF file from storage
|
||||
if ($customSubject->pdf_path && Storage::disk('local')->exists($customSubject->pdf_path)) {
|
||||
Storage::disk('local')->delete($customSubject->pdf_path);
|
||||
}
|
||||
|
||||
$customSubject->delete();
|
||||
|
||||
return back()->with('success', __('Custom subject deleted successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified question from the custom subject.
|
||||
*/
|
||||
public function destroyQuestion(CustomSubject $customSubject, CustomSubjectQuestion $question): RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($question->custom_subject_id !== $customSubject->id || $question->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$question->delete();
|
||||
|
||||
return back()->with('success', __('Question deleted successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a question answer to the user's preferred language.
|
||||
*/
|
||||
public function translateQuestion(Request $request, CustomSubject $customSubject, CustomSubjectQuestion $question): RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($question->custom_subject_id !== $customSubject->id || $question->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (!$question->answer) {
|
||||
return back()->with('error', __('This question has no answer to translate.'));
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'target_language' => ['required', 'string', 'max:10'],
|
||||
]);
|
||||
|
||||
$targetLanguage = $validated['target_language'];
|
||||
|
||||
// Get AI settings
|
||||
$provider = \App\Models\Setting::where('key', 'ai_default_provider')->value('value');
|
||||
$model = \App\Models\Setting::where('key', 'ai_default_model')->value('value');
|
||||
|
||||
if (!$provider) {
|
||||
$provider = config('services.ai.default_provider', 'openrouter');
|
||||
}
|
||||
|
||||
if (!$model) {
|
||||
$model = config('services.ai.providers.' . $provider . '.model', 'inclusionai/ling-3.0-tiny:free');
|
||||
}
|
||||
|
||||
$aiService = new AIService($provider, $model);
|
||||
$result = $aiService->translate($question->answer, $targetLanguage, $customSubject->name);
|
||||
|
||||
if (!$result['success']) {
|
||||
return back()->with('error', __('Failed to translate question: :error', [
|
||||
'error' => $result['error'] ?? __('Unknown error'),
|
||||
]));
|
||||
}
|
||||
|
||||
// Update question with translated answer
|
||||
$question->update([
|
||||
'answer' => $result['content'],
|
||||
]);
|
||||
|
||||
// Track token usage
|
||||
$inputTokens = $result['input_tokens'] ?? 0;
|
||||
$outputTokens = $result['output_tokens'] ?? 0;
|
||||
|
||||
if ($inputTokens > 0 || $outputTokens > 0) {
|
||||
TokenUsage::create([
|
||||
'user_id' => $user->id,
|
||||
'input_tokens' => $inputTokens,
|
||||
'output_tokens' => $outputTokens,
|
||||
'total_tokens' => $inputTokens + $outputTokens,
|
||||
'model' => $model,
|
||||
'provider' => $provider,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', __('Question translated successfully!'));
|
||||
}
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\MessageType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Insight;
|
||||
use App\Models\Subject;
|
||||
use App\Models\TokenUsage;
|
||||
use App\Services\AIService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Controller for managing AI-generated insights.
|
||||
*/
|
||||
class InsightController extends Controller
|
||||
{
|
||||
/**
|
||||
* The AI service instance.
|
||||
*/
|
||||
protected AIService $ai;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*/
|
||||
public function __construct(AIService $ai)
|
||||
{
|
||||
$this->ai = $ai;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of insights for the authenticated user.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$subjectId = $request->input('subject_id');
|
||||
|
||||
/** @var \Illuminate\Pagination\LengthAwarePaginator $insights */
|
||||
$insightsQuery = Insight::where('user_id', $user->id)
|
||||
->with('subject');
|
||||
|
||||
if ($subjectId) {
|
||||
$insightsQuery->where('subject_id', (int) $subjectId);
|
||||
}
|
||||
|
||||
$insights = $insightsQuery->orderByDesc('created_at')
|
||||
->paginate(20);
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Collection<int, Subject> $subjects */
|
||||
$subjects = Subject::where('is_active', true)->get();
|
||||
|
||||
// Token usage statistics
|
||||
$totalInputTokens = TokenUsage::where('user_id', $user->id)->sum('input_tokens');
|
||||
$totalOutputTokens = TokenUsage::where('user_id', $user->id)->sum('output_tokens');
|
||||
$totalTokens = $totalInputTokens + $totalOutputTokens;
|
||||
|
||||
return view('admin.insights.index', compact(
|
||||
'insights',
|
||||
'subjects',
|
||||
'subjectId',
|
||||
'totalInputTokens',
|
||||
'totalOutputTokens',
|
||||
'totalTokens'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new AI insight for the specified subject.
|
||||
*/
|
||||
public function generate(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'subject_id' => ['required', 'exists:subjects,id'],
|
||||
'custom_prompt' => ['nullable', 'string', 'max:500'],
|
||||
'message_type' => ['nullable', 'string', 'max:50'],
|
||||
'language' => ['nullable', 'string', 'in:pt_BR,en,es'],
|
||||
]);
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
/** @var Subject|null $subject */
|
||||
$subject = Subject::findOrFail((int) $validated['subject_id']);
|
||||
|
||||
$messageType = MessageType::tryFrom($validated['message_type'] ?? '') ?? MessageType::AiGenerated;
|
||||
|
||||
$language = $validated['language']
|
||||
?? app()->getLocale()
|
||||
?? 'en';
|
||||
|
||||
// Build the prompt with subject context
|
||||
$prompt = $validated['custom_prompt']
|
||||
?: $this->buildDefaultPrompt($subject, $messageType);
|
||||
|
||||
$context = ['language' => $language];
|
||||
|
||||
if ($subject->introduction) {
|
||||
$context['introduction'] = $subject->introduction;
|
||||
$prompt = "Context about {$subject->name}: {$subject->introduction}\n\n{$prompt}";
|
||||
}
|
||||
|
||||
// Generate message via AI based on type
|
||||
$result = $this->ai->generateInsight($subject->name, $prompt, $context, $messageType);
|
||||
|
||||
if (!$result['success']) {
|
||||
return back()->with('error', __('Failed to generate message: :error', [
|
||||
'error' => $result['error'] ?? __('Unknown error'),
|
||||
]));
|
||||
}
|
||||
|
||||
// Store the generated message
|
||||
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(),
|
||||
]);
|
||||
|
||||
// Track token usage
|
||||
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',
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Message generated successfully!'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a default prompt based on the message type.
|
||||
*/
|
||||
private function buildDefaultPrompt(Subject $subject, MessageType $messageType): string
|
||||
{
|
||||
return 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]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a manually created insight.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'subject_id' => ['required', 'exists:subjects,id'],
|
||||
'content' => ['required', 'string', 'max:5000'],
|
||||
]);
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
Insight::create([
|
||||
'user_id' => $user->id,
|
||||
'subject_id' => (int) $validated['subject_id'],
|
||||
'content' => $validated['content'],
|
||||
'type' => 'manual',
|
||||
'input_tokens' => 0,
|
||||
'output_tokens' => 0,
|
||||
'sent_at' => now(),
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Insight created successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified insight.
|
||||
*/
|
||||
public function destroy(Insight $insight): RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($insight->user_id !== $user->id && !$user->hasRole('superadmin')) {
|
||||
return back()->with('error', __('You do not have permission to delete this insight.'));
|
||||
}
|
||||
|
||||
$insight->delete();
|
||||
|
||||
return back()->with('success', __('Insight deleted successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate an insight to the user's preferred language.
|
||||
*/
|
||||
public function translate(Request $request, Insight $insight): RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
if ($insight->user_id !== $user->id && !$user->hasRole('superadmin')) {
|
||||
return back()->with('error', __('You do not have permission to translate this insight.'));
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'target_language' => ['required', 'string', 'max:10'],
|
||||
]);
|
||||
|
||||
$targetLanguage = $validated['target_language'];
|
||||
|
||||
// Get user's language preference for this subject
|
||||
$userSubject = \App\Models\UserSubject::where('user_id', $insight->user_id)
|
||||
->where('subject_id', $insight->subject_id)
|
||||
->first();
|
||||
|
||||
$language = $userSubject->insight_language ?? $user->locale ?? 'en';
|
||||
|
||||
// Don't translate if already in the target language
|
||||
if ($language === $targetLanguage) {
|
||||
return back()->with('info', __('Insight is already in the selected language.'));
|
||||
}
|
||||
|
||||
$result = $this->ai->translate($insight->content, $targetLanguage, $insight->subject->name);
|
||||
|
||||
if (!$result['success']) {
|
||||
return back()->with('error', __('Failed to translate message: :error', [
|
||||
'error' => $result['error'] ?? __('Unknown error'),
|
||||
]));
|
||||
}
|
||||
|
||||
// Create a new insight with translated content
|
||||
Insight::create([
|
||||
'user_id' => $insight->user_id,
|
||||
'subject_id' => $insight->subject_id,
|
||||
'content' => $result['content'],
|
||||
'type' => $insight->type->value,
|
||||
'input_tokens' => $result['input_tokens'],
|
||||
'output_tokens' => $result['output_tokens'],
|
||||
'sent_at' => now(),
|
||||
]);
|
||||
|
||||
// Track token usage
|
||||
TokenUsage::create([
|
||||
'user_id' => $insight->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',
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Message translated successfully!'));
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Subject;
|
||||
use App\Models\UserInterest;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Manages user interests and AI-generated subject suggestions.
|
||||
*/
|
||||
class InterestController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's interests and AI suggestions.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$interests = $user->userInterests()->orderByDesc('created_at')->get();
|
||||
$suggestions = session('suggestions', []);
|
||||
|
||||
return view('admin.interests.index', compact('interests', 'suggestions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new user interest.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'interest' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$user->userInterests()->create([
|
||||
'interest' => $validated['interest'],
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Interest added successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified user interest.
|
||||
*/
|
||||
public function destroy(UserInterest $userInterest): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $userInterest);
|
||||
|
||||
$userInterest->delete();
|
||||
|
||||
return back()->with('success', __('Interest removed successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate AI subject suggestions based on user interests.
|
||||
*/
|
||||
public function generateSuggestions(Request $request): RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$interests = $user->userInterests()->pluck('interest')->toArray();
|
||||
|
||||
if (empty($interests)) {
|
||||
return back()->with('error', __('Add at least one interest before generating suggestions.'));
|
||||
}
|
||||
|
||||
$interestsList = implode(', ', $interests);
|
||||
|
||||
$prompt = "Com base nos interesses do usuário: {$interestsList}. Sugira de 3 a 5 temas de assuntos de aprendizado que seriam relevantes para este usuário. Para cada tema, forneça: 1) name (formato: 'Aprendizado e Treinamento de [Tema]'), 2) description (máximo 1000 caracteres), 3) introduction (máximo 5000 caracteres). Retorne APENAS um array JSON válido (sem texto adicional, sem markdown, sem code blocks) com objetos contendo esses campos. Todo o conteúdo deve estar em Português do Brasil.";
|
||||
|
||||
/** @var \App\Services\AIService $ai */
|
||||
$ai = app(AIService::class);
|
||||
$result = $ai->generateInsight('Sugestões de assuntos', $prompt, ['language' => 'pt_BR']);
|
||||
|
||||
if (!$result['success']) {
|
||||
return back()->with('error', __('Failed to generate suggestions: :error', ['error' => $result['error'] ?? __('Unknown error')]));
|
||||
}
|
||||
|
||||
$suggestions = $this->extractJsonFromAiResponse($result['content']);
|
||||
|
||||
if (!$suggestions || !is_array($suggestions)) {
|
||||
return back()->with('error', __('Failed to parse AI suggestions. Please try again.'));
|
||||
}
|
||||
|
||||
return back()->with('suggestions', $suggestions)->with('success', __('Suggestions generated successfully.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a subject automatically from a suggestion.
|
||||
*/
|
||||
public function createSubjectFromSuggestion(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'introduction' => ['nullable', 'string', 'max:5000'],
|
||||
]);
|
||||
|
||||
$existing = Subject::where('name', $request->name)->first();
|
||||
|
||||
if ($existing) {
|
||||
return back()->with('error', __('A subject with this name already exists.'));
|
||||
}
|
||||
|
||||
$subject = Subject::create([
|
||||
'user_id' => Auth::id(),
|
||||
'name' => $request->name,
|
||||
'description' => $request->description,
|
||||
'introduction' => $request->introduction,
|
||||
'is_system' => false,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
UserSubject::firstOrCreate(
|
||||
[
|
||||
'user_id' => Auth::id(),
|
||||
'subject_id' => $subject->id,
|
||||
],
|
||||
[
|
||||
'frequency' => 'daily',
|
||||
'preferred_times' => ['09:00'],
|
||||
'insight_language' => Auth::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;
|
||||
}
|
||||
}
|
||||
+511
@@ -0,0 +1,511 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\TelegramUser;
|
||||
use App\Services\TelegramService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Controller for managing Telegram bot integration.
|
||||
* Each user can configure their own Telegram bot for receiving insights.
|
||||
*/
|
||||
class TelegramController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the Telegram configuration page.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
/** @var TelegramUser|null $telegramUser */
|
||||
$telegramUser = $user->telegramUsers()->first();
|
||||
|
||||
return view('admin.telegram.index', compact('telegramUser'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect/configure the user's Telegram bot.
|
||||
*/
|
||||
public function connect(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'bot_token' => ['required', 'string', 'max:255'],
|
||||
'bot_username' => ['nullable', 'string', 'max:255'],
|
||||
'chat_id' => ['required', 'string', 'max:255'],
|
||||
'telegram_username' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
// Test the bot connection before saving
|
||||
$telegramService = new TelegramService($validated['bot_token']);
|
||||
$testResult = $telegramService->sendMessage(
|
||||
$validated['chat_id'],
|
||||
'🤖 *Annunk Bot Connected!*' . "\n\n"
|
||||
. '✅ Your Telegram bot has been successfully connected to Annunk.'
|
||||
. "\n\n" . 'You will now receive learning insights here.'
|
||||
);
|
||||
|
||||
if (!$testResult) {
|
||||
return back()->with('error', __(
|
||||
'Failed to connect. Please check your Bot Token and Chat ID. '
|
||||
. 'Make sure you have started a conversation with your bot first.'
|
||||
))->withInput();
|
||||
}
|
||||
|
||||
TelegramUser::updateOrCreate(
|
||||
['user_id' => $user->id],
|
||||
[
|
||||
'telegram_user_id' => $validated['chat_id'],
|
||||
'bot_token' => $validated['bot_token'],
|
||||
'bot_username' => $validated['bot_username'] ?? null,
|
||||
'chat_id' => $validated['chat_id'],
|
||||
'telegram_username' => $validated['telegram_username'] ?? null,
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
return back()->with('success', __('Telegram bot connected successfully! A test message was sent.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a test message to verify the bot is working.
|
||||
*/
|
||||
public function test(): RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
/** @var TelegramUser|null $telegramUser */
|
||||
$telegramUser = $user->telegramUsers()->first();
|
||||
|
||||
if (!$telegramUser || !$telegramUser->isFullyConfigured()) {
|
||||
return back()->with('error', __('Please configure your Telegram bot first.'));
|
||||
}
|
||||
|
||||
$telegramService = new TelegramService($telegramUser->bot_token);
|
||||
$result = $telegramService->sendMessage(
|
||||
$telegramUser->chat_id,
|
||||
'🧪 *Annunk Test Message*' . "\n\n"
|
||||
. '✅ Your bot is working correctly!'
|
||||
. "\n\n" . 'You will receive learning insights according to your schedule.'
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
return back()->with('success', __('Test message sent successfully! Check your Telegram.'));
|
||||
}
|
||||
|
||||
return back()->with('error', __('Failed to send test message. Please check your bot configuration.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the user's Telegram bot.
|
||||
*/
|
||||
public function disconnect(): RedirectResponse
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$user->telegramUsers()->delete();
|
||||
|
||||
return back()->with('success', __('Telegram bot disconnected successfully.'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user