Files
dockerank2/app/Http/Controllers/Admin/CustomSubjectController.php
2026-09-01 07:49:48 -03:00

368 lines
12 KiB
PHP
Executable File

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