368 lines
12 KiB
PHP
Executable File
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!'));
|
|
}
|
|
}
|