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!')); } }