First Commit

This commit is contained in:
marco
2026-09-01 09:22:30 -03:00
commit a43ae23eb4
407 changed files with 36138 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\AiModel;
use App\Models\AiProvider;
use App\Models\PromptProject;
use App\Services\Ai\AiServiceFactory;
use App\Services\AiAgentService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AiAgentController extends Controller
{
public function __construct(
private readonly AiAgentService $aiAgentService
) {}
public function index(): View
{
$user = auth()->user();
$projectsQuery = PromptProject::with(['provider', 'model'])->latest();
if (!$user || $user->role !== 'superadmin') {
$projectsQuery->where('user_id', $user->id);
}
$projects = $projectsQuery->get();
$providers = AiProvider::where('is_active', true)
->orderBy('name')
->get();
$defaultProviderId = 1;
$defaultModelId = 11;
return view('ai-agent.index', compact('projects', 'providers', 'defaultProviderId', 'defaultModelId'));
}
public function execute(Request $request): RedirectResponse
{
$request->validate([
'project_id' => 'nullable|exists:prompt_projects,id',
'prompt_text' => 'required|string',
'provider_id' => 'required|exists:ai_providers,id',
'model_id' => 'required|exists:ai_models,id',
'temperature' => 'nullable|numeric|min:0|max:1',
'max_tokens' => 'nullable|integer|min:1|max:128000',
]);
$provider = AiProvider::where('id', $request->provider_id)
->where('is_active', true)
->firstOrFail();
$model = AiModel::where('id', $request->model_id)
->where('provider_id', $provider->id)
->where('is_active', true)
->firstOrFail();
$result = $this->aiAgentService->execute(
prompt: $request->prompt_text,
provider: $provider,
model: $model,
temperature: (float) ($request->temperature ?? 0.7),
maxTokens: (int) ($request->max_tokens ?? 1024),
);
return redirect()->route('ai-agent.index')
->with('success', 'Execução concluída com sucesso.')
->with('agent_result', $result)
->withInput([
'project_id' => $request->project_id,
'prompt_text' => $request->prompt_text,
'provider_id' => $request->provider_id,
'model_id' => $request->model_id,
'temperature' => $request->temperature,
'max_tokens' => $request->max_tokens,
]);
}
public function getModels(Request $request)
{
$request->validate([
'provider_id' => 'required|exists:ai_providers,id',
]);
$models = AiModel::where('provider_id', $request->provider_id)
->where('is_active', true)
->orderBy('name')
->get(['id', 'name', 'slug']);
return response()->json($models);
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\AiProvider;
use App\Http\Controllers\Controller;
use App\Http\Requests\AiProvider\StoreAiProviderRequest;
use App\Http\Requests\AiProvider\UpdateAiProviderRequest;
use App\Services\AiProviderService;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class AiProviderController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
private readonly AiProviderService $aiProviderService
) {}
/**
* Display a listing of the resource.
*/
public function index(): View
{
$this->authorize('viewAny', \App\Models\AiProvider::class);
$result = $this->aiProviderService->index();
$providers = $result['data'] ?? [];
return view('ai-providers.index', compact('providers'));
}
/**
* Display the specified resource.
*/
public function show(\App\Models\AiProvider $aiProvider): View
{
$this->authorize('view', $aiProvider);
return view('ai-providers.show', compact('aiProvider'));
}
/**
* Show the form for creating a new resource.
*/
public function create(): View
{
$this->authorize('create', \App\Models\AiProvider::class);
return view('ai-providers.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreAiProviderRequest $request): RedirectResponse
{
$this->authorize('create', \App\Models\AiProvider::class);
$this->aiProviderService->store($request->validated());
return redirect()->route('ai-providers.index')
->with('success', 'Provider created successfully.');
}
/**
* Show the form for editing the specified resource.
*/
public function edit(\App\Models\AiProvider $aiProvider): View
{
$this->authorize('update', $aiProvider);
return view('ai-providers.edit', compact('aiProvider'));
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateAiProviderRequest $request, \App\Models\AiProvider $aiProvider): RedirectResponse
{
$this->authorize('update', $aiProvider);
$this->aiProviderService->update($aiProvider, $request->validated());
return redirect()->route('ai-providers.index')
->with('success', 'Provider updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(\App\Models\AiProvider $aiProvider): RedirectResponse
{
$this->authorize('delete', $aiProvider);
$this->aiProviderService->destroy($aiProvider);
return redirect()->route('ai-providers.index')
->with('success', 'Provider deleted successfully.');
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
class CaptchaController extends Controller
{
/**
* Generate a new arithmetic captcha.
*/
public function generate(Request $request): \Illuminate\Http\JsonResponse
{
$a = random_int(1, 20);
$b = random_int(1, 20);
$operations = ['+', '-', '*'];
$operation = $operations[array_rand($operations)];
if ($operation === '+') {
$answer = $a + $b;
} elseif ($operation === '-') {
$answer = $a - $b;
} else {
$answer = $a * $b;
}
$expression = "{$a} {$operation} {$b} = ?";
Session::put('captcha_answer', (string) $answer);
return response()->json([
'expression' => $expression,
]);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: view('auth.verify-email');
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'password' => ['required', 'confirmed', Password::defaults()],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\RegisterRequest;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(RegisterRequest $request): RedirectResponse
{
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'role' => 'pending',
'is_approved' => false,
]);
event(new Registered($user));
Auth::login($user);
$this->recordDocumentAcceptances($user, $request);
return redirect(route('dashboard'));
}
private function recordDocumentAcceptances(User $user, RegisterRequest $request): void
{
$documents = \App\Models\LegalDocument::all()->groupBy('type');
foreach ($documents as $type => $docs) {
$latest = $docs->sortByDesc('version')->first();
if (!$latest) {
continue;
}
\App\Models\UserDocumentAcceptance::create([
'user_id' => $user->id,
'legal_document_id' => $latest->id,
'document_version' => $latest->version,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'accepted_at' => now(),
]);
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\AiModel;
use App\Models\AiProvider;
use App\Services\ChatService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ChatController extends Controller
{
public function __construct(
private readonly ChatService $chatService
) {}
public function index(): View
{
$providers = AiProvider::where('is_active', true)
->orderBy('name')
->get();
$history = $this->chatService->getHistory();
$defaultProviderId = 1;
$defaultModelId = 2;
return view('chat.index', compact('providers', 'history', 'defaultProviderId', 'defaultModelId'));
}
public function send(Request $request): JsonResponse
{
$request->validate([
'message' => 'required|string',
'provider_id' => 'required|exists:ai_providers,id',
'model_id' => 'required|exists:ai_models,id',
'temperature' => 'nullable|numeric|min:0|max:1',
'max_tokens' => 'nullable|integer|min:1|max:128000',
]);
$provider = AiProvider::where('id', $request->provider_id)
->where('is_active', true)
->firstOrFail();
$model = AiModel::where('id', $request->model_id)
->where('provider_id', $provider->id)
->where('is_active', true)
->firstOrFail();
$result = $this->chatService->sendMessage(
userMessage: $request->message,
provider: $provider,
model: $model,
temperature: (float) ($request->temperature ?? 0.7),
maxTokens: (int) ($request->max_tokens ?? 8192),
);
return response()->json($result);
}
public function reset(): RedirectResponse
{
$this->chatService->reset();
return redirect()->route('chat.index')
->with('success', 'Conversa reiniciada.');
}
public function getModels(Request $request)
{
$request->validate([
'provider_id' => 'required|exists:ai_providers,id',
]);
$models = AiModel::where('provider_id', $request->provider_id)
->where('is_active', true)
->orderBy('name')
->get(['id', 'name', 'slug']);
return response()->json($models);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
abstract class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\AiProvider;
use App\Services\PromptGenerationService;
use App\Services\PromptProjectService;
use Illuminate\View\View;
class DashboardController extends Controller
{
public function __construct(
private readonly PromptProjectService $promptProjectService,
private readonly PromptGenerationService $promptGenerationService
) {}
public function index(): View
{
$userId = auth()->id();
$user = auth()->user();
$stats = [
'projects' => $this->promptProjectService->index()['total'] ?? 0,
'generations' => $this->promptGenerationService->index()->total() ?? 0,
'providers' => AiProvider::count(),
'tokens' => $user && $user->role === 'superadmin'
? \App\Models\PromptGeneration::sum('tokens_used') ?? 0
: \App\Models\PromptGeneration::where('user_id', $userId)->sum('tokens_used') ?? 0,
];
$recentGenerationsQuery = \App\Models\PromptGeneration::with(['project', 'model'])
->latest()
->limit(5);
if (!$user || $user->role !== 'superadmin') {
$recentGenerationsQuery->where('user_id', $userId);
}
$recentGenerations = $recentGenerationsQuery->get();
return view('dashboard', compact('stats', 'recentGenerations'));
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\View\View;
class HomeController extends Controller
{
/**
* Display the welcome page.
*/
public function index(): View
{
return view('welcome');
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Legal;
use App\Http\Controllers\Controller;
use App\Models\CookieConsent;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CookieConsentController extends Controller
{
/**
* Store cookie consent preferences.
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'consents' => ['required', 'array'],
'consents.necessary' => ['required', 'boolean'],
'consents.statistics' => ['nullable', 'boolean'],
'consents.marketing' => ['nullable', 'boolean'],
'consents.preferences' => ['nullable', 'boolean'],
]);
$consent = CookieConsent::updateOrCreate(
[
'user_id' => Auth::id(),
'session_id' => $request->session()->getId(),
],
[
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'consents' => $validated['consents'],
'accepted_at' => now(),
]
);
return response()->json(['success' => true, 'consent_id' => $consent->id]);
}
/**
* Get current cookie consent preferences.
*/
public function show(Request $request)
{
$consent = CookieConsent::where('user_id', Auth::id())
->orWhere('session_id', $request->session()->getId())
->latest()
->first();
return response()->json($consent ? $consent->consents : null);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Legal;
use App\Http\Controllers\Controller;
use App\Models\LegalDocument;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class LegalDocumentController extends Controller
{
/**
* Display a listing of legal documents.
*/
public function index(Request $request)
{
$this->authorize('viewAny', LegalDocument::class);
$documents = LegalDocument::orderBy('type')->orderByDesc('version')->paginate(20);
return view('legal.documents.index', compact('documents'));
}
/**
* Show the form for editing a legal document.
*/
public function edit(string $type)
{
$document = LegalDocument::where('type', $type)->latest('version')->first();
if (!$document) {
$document = new LegalDocument(['type' => $type]);
}
$this->authorize('update', $document);
return view('legal.documents.edit', compact('document'));
}
/**
* Update or create a legal document.
*/
public function update(Request $request, string $type)
{
$this->authorize('create', LegalDocument::class);
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
'version' => ['required', 'string', 'max:50'],
'published_at' => ['nullable', 'date'],
]);
$document = LegalDocument::where('type', $type)->latest('version')->first();
if ($document && $document->version === $validated['version']) {
$document->update([
'title' => $validated['title'],
'content' => $validated['content'],
'published_at' => $validated['published_at'] ? now() : null,
]);
} else {
$document = LegalDocument::create(array_merge($validated, [
'type' => $type,
'published_by' => Auth::id(),
'published_at' => $validated['published_at'] ?? now(),
]));
}
return redirect()->route('legal.documents.index')->with('success', 'Documento atualizado com sucesso.');
}
/**
* Display the legal document page.
*/
public function show(string $type)
{
$document = LegalDocument::where('type', $type)->latest('version')->firstOrFail();
return view('legal.documents.show', compact('document'));
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\PromptGeneration;
use App\Models\PromptProject;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ProfileController extends Controller
{
public function index(): View
{
$user = auth()->user();
$stats = [
'projects' => PromptProject::where('user_id', $user->id)->count(),
'generations' => PromptGeneration::where('user_id', $user->id)->count(),
'tokens' => PromptGeneration::where('user_id', $user->id)->sum('tokens_used') ?? 0,
];
return view('profile.index', compact('user', 'stats'));
}
public function edit(): View
{
$user = auth()->user();
return view('profile.edit', compact('user'));
}
public function update(Request $request): RedirectResponse
{
$user = auth()->user();
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users,email,' . $user->id,
'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
if ($request->hasFile('avatar')) {
$path = $request->file('avatar')->store('avatars', 'public');
$user->avatar = $path;
}
$user->name = $validated['name'];
$user->email = $validated['email'];
$user->save();
return redirect()->route('profile.index')
->with('success', 'Perfil atualizado com sucesso.');
}
}
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\PromptGeneration;
use App\Http\Controllers\Controller;
use App\Http\Requests\PromptGeneration\GeneratePromptRequest;
use App\Models\PromptGeneration;
use App\Services\PromptGenerationService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class PromptGenerationController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
private readonly PromptGenerationService $promptGenerationService
) {}
/**
* Display a listing of prompt generations.
*/
public function index(): View
{
$generations = $this->promptGenerationService->index();
return view('prompt-generations.index', compact('generations'));
}
/**
* Generate a new prompt.
*/
public function generate(GeneratePromptRequest $request): RedirectResponse
{
$result = $this->promptGenerationService->generate($request->validated());
return redirect()->route('prompt-generations.index')
->with('success', 'Prompt generated successfully.')
->with('generation', $result);
}
/**
* Remove the specified prompt generation.
*/
public function destroy(Request $request, PromptGeneration $generation): RedirectResponse
{
$this->authorize('delete', $generation);
$this->promptGenerationService->destroy($generation);
return redirect()->back()->with('success', 'Prompt generation deleted successfully.');
}
}
@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\PromptProject;
use App\Http\Controllers\Controller;
use App\Http\Requests\PromptProject\StorePromptProjectRequest;
use App\Http\Requests\PromptProject\UpdatePromptProjectRequest;
use App\Models\AiProvider;
use App\Services\PromptProjectService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class PromptProjectController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
private readonly PromptProjectService $promptProjectService
) {}
/**
* Display a listing of the resource.
*/
public function index(): View
{
$result = $this->promptProjectService->index();
$projects = $result['data'] ?? [];
return view('prompt-projects.index', compact('projects'));
}
/**
* Show the form for creating a new resource.
*/
public function create(): View
{
$this->authorize('create', \App\Models\PromptProject::class);
$providers = AiProvider::where('is_active', true)->get();
return view('prompt-projects.create', compact('providers'));
}
/**
* Store a newly created resource in storage.
*/
public function store(StorePromptProjectRequest $request): RedirectResponse
{
$this->authorize('create', \App\Models\PromptProject::class);
$this->promptProjectService->store($request->validated());
return redirect()->route('prompt-projects.index')
->with('success', 'Project created successfully.');
}
/**
* Display the specified resource.
*/
public function show(\App\Models\PromptProject $promptProject): View
{
$this->authorize('view', $promptProject);
$providerModels = \App\Models\AiModel::where('provider_id', $promptProject->provider_id)
->where('is_active', true)
->get();
$models = $providerModels->isNotEmpty()
? $providerModels
: \App\Models\AiModel::where('is_active', true)->get();
$generations = \App\Models\PromptGeneration::where('project_id', $promptProject->id)
->latest()
->take(5)
->get();
return view('prompt-projects.show', compact('promptProject', 'models', 'generations'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(\App\Models\PromptProject $promptProject): View
{
$this->authorize('update', $promptProject);
$providers = AiProvider::where('is_active', true)->get();
return view('prompt-projects.edit', compact('promptProject', 'providers'));
}
/**
* Update the specified resource in storage.
*/
public function update(UpdatePromptProjectRequest $request, \App\Models\PromptProject $promptProject): RedirectResponse
{
$this->authorize('update', $promptProject);
$this->promptProjectService->update($promptProject, $request->validated());
return redirect()->route('prompt-projects.show', $promptProject)
->with('success', 'Project updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(\App\Models\PromptProject $promptProject): RedirectResponse
{
$this->authorize('delete', $promptProject);
$this->promptProjectService->destroy($promptProject);
return redirect()->route('prompt-projects.index')
->with('success', 'Project deleted successfully.');
}
/**
* Generate prompt for the specified project.
*/
public function generate(\App\Models\PromptProject $promptProject, Request $request): RedirectResponse
{
$this->authorize('update', $promptProject);
$model = null;
if ($request->filled('model')) {
$modelSlug = $request->input('model');
if ($modelSlug === 'outros') {
$customModel = $request->input('custom_model');
if ($customModel) {
$model = $customModel;
}
} else {
$model = \App\Models\AiModel::where('slug', $modelSlug)
->where('is_active', true)
->first();
}
}
$this->promptProjectService->generate($promptProject, $model, Auth::id());
return redirect()->route('prompt-projects.show', $promptProject)
->with('success', 'Prompt generated successfully.');
}
/**
* Get built prompt for the specified project.
*/
public function getPrompt(\App\Models\PromptProject $promptProject): \Illuminate\Http\JsonResponse
{
$this->authorize('view', $promptProject);
$prompt = $this->promptProjectService->buildPrompt($promptProject);
return response()->json(['prompt' => $prompt]);
}
}
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\PromptTemplate;
use App\Http\Controllers\Controller;
use App\Http\Requests\PromptTemplate\StorePromptTemplateRequest;
use App\Http\Requests\PromptTemplate\UpdatePromptTemplateRequest;
use App\Models\PromptTemplate;
use App\Services\PromptTemplateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class PromptTemplateController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
private readonly PromptTemplateService $promptTemplateService
) {}
/**
* Display a listing of the resource.
*/
public function index(): View
{
$this->authorize('viewAny', \App\Models\PromptTemplate::class);
$result = $this->promptTemplateService->index();
$templates = $result['data'] ?? [];
return view('prompt-templates.index', compact('templates'));
}
/**
* Show the form for creating a new resource.
*/
public function create(): View
{
$this->authorize('create', \App\Models\PromptTemplate::class);
return view('prompt-templates.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(StorePromptTemplateRequest $request): RedirectResponse
{
$this->authorize('create', \App\Models\PromptTemplate::class);
$data = $request->validated();
if (isset($data['template_data']) && is_string($data['template_data'])) {
$data['template_data'] = json_decode($data['template_data'], true);
}
$this->promptTemplateService->store($data);
return redirect()->route('prompt-templates.index')
->with('success', 'Template created successfully.');
}
/**
* Show the form for editing the specified resource.
*/
public function edit(\App\Models\PromptTemplate $promptTemplate): View
{
$this->authorize('update', $promptTemplate);
return view('prompt-templates.edit', compact('promptTemplate'));
}
/**
* Update the specified resource in storage.
*/
public function update(UpdatePromptTemplateRequest $request, \App\Models\PromptTemplate $promptTemplate): RedirectResponse
{
$this->authorize('update', $promptTemplate);
$data = $request->validated();
if (isset($data['template_data']) && is_string($data['template_data'])) {
$data['template_data'] = json_decode($data['template_data'], true);
}
$this->promptTemplateService->update($promptTemplate, $data);
return redirect()->route('prompt-templates.index')
->with('success', 'Template updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(\App\Models\PromptTemplate $promptTemplate): RedirectResponse
{
$this->authorize('delete', $promptTemplate);
$this->promptTemplateService->destroy($promptTemplate);
return redirect()->route('prompt-templates.index')
->with('success', 'Template deleted successfully.');
}
/**
* Get all templates as JSON for the dropdown.
*/
public function api(): JsonResponse
{
$templates = PromptTemplate::where('is_public', true)
->orWhere('user_id', auth()->id())
->get(['name', 'template_data']);
$result = [];
foreach ($templates as $template) {
$data = $template->template_data;
$result[$template->name] = [
'title' => $data['title'] ?? $template->name,
'objective' => $data['objective'] ?? '',
'context' => $data['context'] ?? '',
'persona' => $data['persona'] ?? '',
'tone' => $data['tone'] ?? '',
'constraints' => $data['constraints'] ?? '',
'output_format' => $data['output_format'] ?? '',
'examples' => $data['examples'] ?? '',
'variables' => $data['variables'] ?? '',
'temperature' => $data['temperature'] ?? 0.7,
'max_tokens' => $data['max_tokens'] ?? 1024,
];
}
return response()->json($result);
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Tutorial;
use App\Http\Controllers\Controller;
use App\Models\TutorialLesson;
use App\Models\TutorialQuestion;
use App\Models\TutorialUserProgress;
use App\Services\TutorialService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Barryvdh\DomPDF\Facade\Pdf;
class TutorialController extends Controller
{
public function __construct(
private readonly TutorialService $tutorialService
) {}
public function index(): View
{
$lessons = TutorialLesson::orderBy('order')->get();
$progress = TutorialUserProgress::where('user_id', Auth::id())->first();
return view('tutorial.index', compact('lessons', 'progress'));
}
public function lesson(int $id): View
{
$lesson = TutorialLesson::findOrFail($id);
$lessons = TutorialLesson::orderBy('order')->get();
$progress = TutorialUserProgress::where('user_id', Auth::id())->first();
$completedLessons = $progress?->completed_lessons ?? [];
if (!in_array($id, $completedLessons)) {
$completedLessons[] = $id;
if (!$progress) {
$progress = TutorialUserProgress::create(['user_id' => Auth::id(), 'completed_lessons' => $completedLessons]);
} else {
$progress->update(['completed_lessons' => $completedLessons]);
}
}
return view('tutorial.lesson', compact('lesson', 'lessons', 'progress'));
}
public function quiz(): View
{
$questions = TutorialQuestion::orderBy('order')->get();
$progress = TutorialUserProgress::where('user_id', Auth::id())->first();
return view('tutorial.quiz', compact('questions', 'progress'));
}
public function submitQuiz(Request $request): View
{
$request->validate([
'answers' => 'required|array',
'answers.*' => 'required|string',
]);
$questions = TutorialQuestion::orderBy('order')->get();
$score = 0;
$answers = $request->input('answers', []);
$results = [];
foreach ($questions as $question) {
$isCorrect = isset($answers[$question->id]) && $answers[$question->id] === $question->correct_answer;
if ($isCorrect) {
$score++;
}
$results[] = [
'question' => $question->question,
'your_answer' => $answers[$question->id] ?? null,
'correct_answer' => $question->correct_answer,
'is_correct' => $isCorrect,
'explanation' => $question->explanation,
];
}
$passed = $score >= 6;
$progress = TutorialUserProgress::where('user_id', Auth::id())->first();
if (!$progress) {
$progress = TutorialUserProgress::create(['user_id' => Auth::id()]);
}
$progress->update([
'quiz_answers' => $answers,
'quiz_score' => $score,
'quiz_passed' => $passed,
'quiz_completed_at' => now(),
]);
return view('tutorial.result', compact('score', 'results', 'passed', 'progress'));
}
public function certificate(): \Illuminate\Http\Response
{
$user = Auth::user();
$progress = TutorialUserProgress::where('user_id', $user->id)->firstOrFail();
if (!$progress->quiz_passed) {
return redirect()->route('tutorial.quiz')->with('error', 'Você precisa passar no quiz para gerar o certificado.');
}
$progress->update(['certificate_generated_at' => now()]);
$pdf = Pdf::loadView('tutorial.certificate', compact('user', 'progress'));
$pdf->setPaper('a4', 'landscape');
return $pdf->download('certificado-engenharia-prompt-gprompt.pdf');
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UserController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'verified', 'user.approved']);
}
public function index(): View
{
$this->authorize('viewAny', User::class);
$users = User::query()
->select(['id', 'name', 'email', 'role', 'is_approved', 'created_at'])
->orderByDesc('created_at')
->paginate(15);
$users->each(function ($user) {
$user->tokens_used = $user->generations()
->where('status', 'completed')
->sum('tokens_used') ?? 0;
$user->generations_count = $user->generations()->count();
});
return view('users.index', compact('users'));
}
public function edit(User $user): View
{
$this->authorize('update', $user);
return view('users.edit', compact('user'));
}
public function update(Request $request, User $user): RedirectResponse
{
$this->authorize('update', $user);
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users,email,' . $user->id,
'role' => 'required|in:superadmin,user',
'is_approved' => 'required|boolean',
'password' => 'nullable|string|min:6|confirmed',
]);
if (empty($validated['password'])) {
unset($validated['password']);
}
$user->update($validated);
return redirect()->route('users.index')
->with('success', 'Usuário atualizado com sucesso.');
}
public function destroy(User $user): RedirectResponse
{
$this->authorize('delete', $user);
if ($user->id === auth()->id()) {
return redirect()->route('users.index')
->with('error', 'Você não pode excluir a si mesmo.');
}
$user->delete();
return redirect()->route('users.index')
->with('success', 'Usuário excluído com sucesso.');
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Models\LegalDocument;
use App\Models\UserDocumentAcceptance;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class EnsureDocumentsAccepted
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next)
{
if (Auth::check() && Auth::user()->role !== 'superadmin') {
$user = Auth::user();
$documents = LegalDocument::all()->groupBy('type');
foreach ($documents as $type => $docs) {
$latest = $docs->sortByDesc('version')->first();
if (!$latest) {
continue;
}
$accepted = UserDocumentAcceptance::where('user_id', $user->id)
->where('legal_document_id', $latest->id)
->exists();
if (!$accepted) {
return redirect()->route('legal.document.show', $latest->type)
->with('warning', 'Você precisa aceitar a versão atual dos documentos legais para continuar.');
}
}
}
return $next($request);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsApproved
{
/**
* Handle an incoming request.
*
* @param Closure(Request): Response $next
*/
public function handle(Request $request, Closure $next): Response
{
if (Auth::check() && !Auth::user()->is_approved && Auth::user()->role !== 'superadmin') {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')
->with('error', 'Sua conta está aguardando aprovação do SuperAdmin.');
}
return $next($request);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\AiProvider;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreAiProviderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255', Rule::unique('ai_providers', 'name')->whereNull('deleted_at')],
'slug' => ['required', 'string', 'max:255', Rule::unique('ai_providers', 'slug')->whereNull('deleted_at')],
'description' => ['nullable', 'string'],
'api_key' => ['required', 'string', 'max:255'],
'base_url' => ['required', 'url', 'max:255'],
'is_active' => ['boolean'],
'config' => ['nullable', 'array'],
];
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\AiProvider;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateAiProviderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:255', Rule::unique('ai_providers', 'name')->ignore($this->route('ai_provider'))],
'slug' => ['sometimes', 'string', 'max:255', Rule::unique('ai_providers', 'slug')->ignore($this->route('ai_provider'))],
'description' => ['nullable', 'string'],
'api_key' => ['sometimes', 'string', 'max:255'],
'base_url' => ['sometimes', 'url', 'max:255'],
'is_active' => ['boolean'],
'config' => ['nullable', 'array'],
];
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Auth;
use App\Rules\Captcha;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
/**
* Form request for user login with arithmetic captcha validation.
*/
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
'captcha' => ['required', 'string', 'max:4', new Captcha()],
];
}
/**
* Get custom messages for validator errors.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'email.required' => 'O e-mail é obrigatório.',
'email.email' => 'Informe um e-mail válido.',
'password.required' => 'A senha é obrigatória.',
'captcha.required' => 'O captcha é obrigatório.',
'captcha.size' => 'O captcha deve ter :size caracteres.',
'captcha' => 'O captcha informado está incorreto.',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'email' => 'e-mail',
'password' => 'senha',
'captcha' => 'captcha',
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Auth;
use App\Rules\Captcha;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
/**
* Form request for user registration with arithmetic captcha validation.
*/
class RegisterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'confirmed', Password::defaults()],
'captcha' => ['required', 'string', 'max:4', new Captcha()],
'terms_of_service' => ['required', 'accepted'],
'privacy_policy' => ['required', 'accepted'],
];
}
/**
* Get custom messages for validator errors.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'name.required' => 'O nome é obrigatório.',
'name.max' => 'O nome não pode ultrapassar 255 caracteres.',
'email.required' => 'O e-mail é obrigatório.',
'email.email' => 'Informe um e-mail válido.',
'email.unique' => 'Este e-mail já está cadastrado.',
'password.required' => 'A senha é obrigatória.',
'password.confirmed' => 'A confirmação de senha não confere.',
'captcha.required' => 'O captcha é obrigatório.',
'captcha.size' => 'O captcha deve ter :size caracteres.',
'captcha' => 'O captcha informado está incorreto.',
'terms_of_service.accepted' => 'Você deve aceitar os Termos de Uso para continuar.',
'privacy_policy.accepted' => 'Você deve aceitar a Política de Privacidade para continuar.',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'name' => 'nome',
'email' => 'e-mail',
'password' => 'senha',
'password_confirmation' => 'confirmação de senha',
'captcha' => 'captcha',
'terms_of_service' => 'Termos de Uso',
'privacy_policy' => 'Política de Privacidade',
];
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\PromptGeneration;
use Illuminate\Foundation\Http\FormRequest;
class GeneratePromptRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'project_id' => ['required', 'exists:prompt_projects,id'],
'provider_id' => ['required', 'exists:ai_providers,id'],
'model_id' => ['required', 'exists:ai_models,id'],
'temperature' => ['nullable', 'numeric', 'min:0', 'max:1'],
'max_tokens' => ['nullable', 'integer', 'min:1'],
];
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\PromptProject;
use Illuminate\Foundation\Http\FormRequest;
class StorePromptProjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'company_name' => ['nullable', 'string', 'max:255'],
'company_address' => ['nullable', 'string'],
'company_whatsapp' => ['nullable', 'string', 'max:255'],
'company_website' => ['nullable', 'url', 'max:255'],
'company_social_media' => ['nullable', 'string'],
'company_business_activities' => ['nullable', 'string'],
'company_services_products' => ['nullable', 'string'],
'objective' => ['required', 'string'],
'context' => ['nullable', 'string'],
'persona' => ['nullable', 'string', 'max:255'],
'tone' => ['nullable', 'string', 'max:255'],
'constraints' => ['nullable', 'string'],
'output_format' => ['nullable', 'string'],
'examples' => ['nullable', 'string'],
'variables' => ['nullable', 'string'],
'temperature' => ['nullable', 'numeric', 'min:0', 'max:1'],
'max_tokens' => ['nullable', 'integer', 'min:1'],
'provider_id' => ['nullable', 'exists:ai_providers,id'],
'model_id' => ['nullable', 'exists:ai_models,id'],
];
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\PromptProject;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePromptProjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => ['sometimes', 'string', 'max:255'],
'company_name' => ['nullable', 'string', 'max:255'],
'company_address' => ['nullable', 'string'],
'company_whatsapp' => ['nullable', 'string', 'max:255'],
'company_website' => ['nullable', 'url', 'max:255'],
'company_social_media' => ['nullable', 'string'],
'company_business_activities' => ['nullable', 'string'],
'company_services_products' => ['nullable', 'string'],
'objective' => ['sometimes', 'string'],
'context' => ['nullable', 'string'],
'persona' => ['nullable', 'string', 'max:255'],
'tone' => ['nullable', 'string', 'max:255'],
'constraints' => ['nullable', 'string'],
'output_format' => ['nullable', 'string'],
'examples' => ['nullable', 'string'],
'variables' => ['nullable', 'string'],
'temperature' => ['nullable', 'numeric', 'min:0', 'max:1'],
'max_tokens' => ['nullable', 'integer', 'min:1'],
'provider_id' => ['nullable', 'exists:ai_providers,id'],
'model_id' => ['nullable', 'exists:ai_models,id'],
];
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\PromptTemplate;
use Illuminate\Foundation\Http\FormRequest;
class StorePromptTemplateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'category' => ['nullable', 'string', 'max:255'],
'template_data' => ['required', 'string'],
'is_public' => ['boolean'],
];
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\PromptTemplate;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePromptTemplateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'category' => ['nullable', 'string', 'max:255'],
'template_data' => ['required', 'string'],
'is_public' => ['boolean'],
];
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class AiModel extends Model
{
use \Illuminate\Database\Eloquent\SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'provider_id',
'name',
'slug',
'max_tokens',
'temperature_support',
'is_active',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'temperature_support' => 'boolean',
'is_active' => 'boolean',
];
}
/**
* Get the AI provider that owns the model.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\AiProvider, \App\Models\AiModel>
*/
public function provider(): BelongsTo
{
return $this->belongsTo(AiProvider::class);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class AiProvider extends Model
{
use \Illuminate\Database\Eloquent\SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'slug',
'description',
'api_key',
'base_url',
'is_active',
'config',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_active' => 'boolean',
'config' => 'array',
];
}
/**
* Get all of the models for the AI provider.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\AiModel>
*/
public function models(): HasMany
{
return $this->hasMany(AiModel::class, 'provider_id');
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CookieConsent extends Model
{
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'user_id',
'session_id',
'ip_address',
'user_agent',
'consents',
'accepted_at',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'consents' => 'array',
'accepted_at' => 'datetime',
];
}
/**
* Get the user that owns the cookie consent.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User, \App\Models\CookieConsent>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class LegalDocument extends Model
{
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'type',
'title',
'content',
'version',
'published_at',
'published_by',
];
/**
* The attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'published_at' => 'datetime',
];
}
/**
* Get the user that published the document.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User, \App\Models\LegalDocument>
*/
public function publishedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'published_by');
}
/**
* Get the user document acceptances for the legal document.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\UserDocumentAcceptance, \App\Models\LegalDocument>
*/
public function acceptances(): HasMany
{
return $this->hasMany(UserDocumentAcceptance::class);
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class PromptGeneration extends Model
{
use \Illuminate\Database\Eloquent\SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'project_id',
'user_id',
'provider_id',
'model_id',
'generated_prompt',
'input_data',
'output_data',
'tokens_used',
'generation_time',
'status',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'input_data' => 'array',
'output_data' => 'array',
'tokens_used' => 'integer',
'generation_time' => 'decimal:3',
];
}
/**
* Get the prompt project that owns the generation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\PromptProject, \App\Models\PromptGeneration>
*/
public function project(): BelongsTo
{
return $this->belongsTo(PromptProject::class);
}
/**
* Get the user that owns the generation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User, \App\Models\PromptGeneration>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the AI provider for the generation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\AiProvider, \App\Models\PromptGeneration>
*/
public function provider(): BelongsTo
{
return $this->belongsTo(AiProvider::class);
}
/**
* Get the AI model for the generation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\AiModel, \App\Models\PromptGeneration>
*/
public function model(): BelongsTo
{
return $this->belongsTo(AiModel::class);
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class PromptProject extends Model
{
use \Illuminate\Database\Eloquent\SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'user_id',
'title',
'company_name',
'company_address',
'company_whatsapp',
'company_website',
'company_social_media',
'company_business_activities',
'company_services_products',
'objective',
'context',
'persona',
'tone',
'constraints',
'output_format',
'examples',
'variables',
'temperature',
'max_tokens',
'provider_id',
'model_id',
'generated_prompt',
'status',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'examples' => 'array',
'variables' => 'array',
'temperature' => 'decimal:2',
];
}
/**
* Get the user that owns the prompt project.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User, \App\Models\PromptProject>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the AI provider for the prompt project.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\AiProvider, \App\Models\PromptProject>
*/
public function provider(): BelongsTo
{
return $this->belongsTo(AiProvider::class);
}
/**
* Get the AI model for the prompt project.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\AiModel, \App\Models\PromptProject>
*/
public function model(): BelongsTo
{
return $this->belongsTo(AiModel::class);
}
/**
* Get all of the generations for the prompt project.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\PromptGeneration>
*/
public function generations(): HasMany
{
return $this->hasMany(PromptGeneration::class);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class PromptTemplate extends Model
{
use \Illuminate\Database\Eloquent\SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'description',
'category',
'template_data',
'is_public',
'user_id',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_public' => 'boolean',
'template_data' => 'array',
];
}
/**
* Get the user that owns the prompt template.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User, \App\Models\PromptTemplate>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class TutorialLesson extends Model
{
protected $fillable = [
'title',
'content',
'example_bad',
'example_good',
'order',
];
protected function casts(): array
{
return [
'order' => 'integer',
];
}
public function questions(): HasMany
{
return $this->hasMany(TutorialQuestion::class);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TutorialQuestion extends Model
{
protected $fillable = [
'question',
'options',
'correct_answer',
'explanation',
'order',
];
protected function casts(): array
{
return [
'options' => 'array',
'order' => 'integer',
];
}
public function lesson(): BelongsTo
{
return $this->belongsTo(TutorialLesson::class);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TutorialUserProgress extends Model
{
protected $fillable = [
'user_id',
'completed_lessons',
'quiz_answers',
'quiz_score',
'quiz_passed',
'quiz_completed_at',
'certificate_generated_at',
];
protected function casts(): array
{
return [
'completed_lessons' => 'array',
'quiz_answers' => 'array',
'quiz_score' => 'integer',
'quiz_passed' => 'boolean',
'quiz_completed_at' => 'datetime',
'certificate_generated_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password', 'role', 'is_approved', 'avatar'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/**
* The attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_approved' => 'boolean',
'role' => 'string',
];
}
/**
* Get all of the prompt projects for the user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\PromptProject>
*/
public function projects(): HasMany
{
return $this->hasMany(PromptProject::class);
}
/**
* Get all of the prompt generations for the user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\PromptGeneration>
*/
public function generations(): HasMany
{
return $this->hasMany(PromptGeneration::class);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserDocumentAcceptance extends Model
{
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'user_id',
'legal_document_id',
'document_version',
'ip_address',
'user_agent',
'accepted_at',
];
/**
* The attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'accepted_at' => 'datetime',
];
}
/**
* Get the user that accepted the document.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User, \App\Models\UserDocumentAcceptance>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the legal document that was accepted.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\LegalDocument, \App\Models\UserDocumentAcceptance>
*/
public function document(): BelongsTo
{
return $this->belongsTo(LegalDocument::class, 'legal_document_id');
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\AiProvider;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class AiProviderPolicy
{
/**
* Determine whether the user can view any AI providers.
*/
public function viewAny(User $user): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to view AI providers.');
}
/**
* Determine whether the user can view the AI provider.
*/
public function view(User $user, AiProvider $aiProvider): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to view this AI provider.');
}
/**
* Determine whether the user can create AI providers.
*/
public function create(User $user): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to create AI providers.');
}
/**
* Determine whether the user can update the AI provider.
*/
public function update(User $user, AiProvider $aiProvider): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to update this AI provider.');
}
/**
* Determine whether the user can delete the AI provider.
*/
public function delete(User $user, AiProvider $aiProvider): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to delete this AI provider.');
}
/**
* Determine whether the user can restore the AI provider.
*/
public function restore(User $user, AiProvider $aiProvider): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to restore this AI provider.');
}
/**
* Determine whether the user can permanently delete the AI provider.
*/
public function forceDelete(User $user, AiProvider $aiProvider): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to permanently delete this AI provider.');
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\LegalDocument;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\Access\Response;
class LegalDocumentPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->role === 'superadmin';
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, LegalDocument $document): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->role === 'superadmin';
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, LegalDocument $document): bool
{
return $user->role === 'superadmin';
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, LegalDocument $document): bool
{
return $user->role === 'superadmin';
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, LegalDocument $document): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, LegalDocument $document): bool
{
return false;
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\PromptGeneration;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class PromptGenerationPolicy
{
/**
* Determine whether the user can view any prompt generations.
*/
public function viewAny(User $user): Response
{
return in_array($user->role, ['superadmin', 'user'], true)
? Response::allow()
: Response::deny('You do not have permission to view prompt generations.');
}
/**
* Determine whether the user can view the prompt generation.
*/
public function view(User $user, PromptGeneration $promptGeneration): Response
{
return $user->role === 'superadmin' || $promptGeneration->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to view this prompt generation.');
}
/**
* Determine whether the user can create prompt generations.
*/
public function create(User $user): Response
{
return $user->role === 'superadmin' || $user->role === 'user'
? Response::allow()
: Response::deny('You do not have permission to create prompt generations.');
}
/**
* Determine whether the user can delete the prompt generation.
*/
public function delete(User $user, PromptGeneration $promptGeneration): Response
{
return $user->role === 'superadmin' || $promptGeneration->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to delete this prompt generation.');
}
/**
* Determine whether the user can restore the prompt generation.
*/
public function restore(User $user, PromptGeneration $promptGeneration): Response
{
return $user->role === 'superadmin' || $promptGeneration->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to restore this prompt generation.');
}
/**
* Determine whether the user can permanently delete the prompt generation.
*/
public function forceDelete(User $user, PromptGeneration $promptGeneration): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to permanently delete this prompt generation.');
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\PromptProject;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class PromptProjectPolicy
{
/**
* Determine whether the user can view any prompt projects.
*/
public function viewAny(User $user): Response
{
return in_array($user->role, ['superadmin', 'user'], true)
? Response::allow()
: Response::deny('You do not have permission to view prompt projects.');
}
/**
* Determine whether the user can view the prompt project.
*/
public function view(User $user, PromptProject $promptProject): Response
{
return $user->role === 'superadmin' || $promptProject->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to view this prompt project.');
}
/**
* Determine whether the user can create prompt projects.
*/
public function create(User $user): Response
{
return $user->role === 'superadmin' || $user->role === 'user'
? Response::allow()
: Response::deny('You do not have permission to create prompt projects.');
}
/**
* Determine whether the user can update the prompt project.
*/
public function update(User $user, PromptProject $promptProject): Response
{
return $user->role === 'superadmin' || $promptProject->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to update this prompt project.');
}
/**
* Determine whether the user can delete the prompt project.
*/
public function delete(User $user, PromptProject $promptProject): Response
{
return $user->role === 'superadmin' || $promptProject->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to delete this prompt project.');
}
/**
* Determine whether the user can restore the prompt project.
*/
public function restore(User $user, PromptProject $promptProject): Response
{
return $user->role === 'superadmin' || $promptProject->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to restore this prompt project.');
}
/**
* Determine whether the user can permanently delete the prompt project.
*/
public function forceDelete(User $user, PromptProject $promptProject): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to permanently delete this prompt project.');
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\PromptTemplate;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class PromptTemplatePolicy
{
/**
* Determine whether the user can view any prompt templates.
*/
public function viewAny(User $user): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to view prompt templates.');
}
/**
* Determine whether the user can view the prompt template.
*/
public function view(User $user, PromptTemplate $promptTemplate): Response
{
return $user->role === 'superadmin' || $promptTemplate->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to view this prompt template.');
}
/**
* Determine whether the user can create prompt templates.
*/
public function create(User $user): Response
{
return $user->role === 'superadmin' || $user->role === 'user'
? Response::allow()
: Response::deny('You do not have permission to create prompt templates.');
}
/**
* Determine whether the user can update the prompt template.
*/
public function update(User $user, PromptTemplate $promptTemplate): Response
{
return $user->role === 'superadmin' || $promptTemplate->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to update this prompt template.');
}
/**
* Determine whether the user can delete the prompt template.
*/
public function delete(User $user, PromptTemplate $promptTemplate): Response
{
return $user->role === 'superadmin' || $promptTemplate->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to delete this prompt template.');
}
/**
* Determine whether the user can restore the prompt template.
*/
public function restore(User $user, PromptTemplate $promptTemplate): Response
{
return $user->role === 'superadmin' || $promptTemplate->user_id === $user->id
? Response::allow()
: Response::deny('You do not have permission to restore this prompt template.');
}
/**
* Determine whether the user can permanently delete the prompt template.
*/
public function forceDelete(User $user, PromptTemplate $promptTemplate): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to permanently delete this prompt template.');
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class UserPolicy
{
public function viewAny(User $user): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to view users.');
}
public function view(User $user, User $target): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to view this user.');
}
public function create(User $user): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to create users.');
}
public function update(User $user, User $target): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to update this user.');
}
public function delete(User $user, User $target): Response
{
return $user->role === 'superadmin'
? Response::allow()
: Response::deny('You do not have permission to delete this user.');
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Providers;
use App\Models\AiProvider;
use App\Models\LegalDocument;
use App\Models\PromptGeneration;
use App\Models\PromptProject;
use App\Models\PromptTemplate;
use App\Models\User;
use App\Policies\AiProviderPolicy;
use App\Policies\LegalDocumentPolicy;
use App\Policies\PromptGenerationPolicy;
use App\Policies\PromptProjectPolicy;
use App\Policies\PromptTemplatePolicy;
use App\Policies\UserPolicy;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
$this->registerPolicies();
Gate::define('superadmin', function (User $user) {
return $user->role === 'superadmin';
});
Gate::before(function (User $user, string $ability) {
if ($user->role === 'superadmin') {
return true;
}
});
Gate::resource(PromptProject::class, PromptProjectPolicy::class);
Gate::resource(PromptGeneration::class, PromptGenerationPolicy::class);
Gate::resource(AiProvider::class, AiProviderPolicy::class);
Gate::resource(User::class, UserPolicy::class);
Gate::resource(PromptTemplate::class, PromptTemplatePolicy::class);
Gate::resource(LegalDocument::class, LegalDocumentPolicy::class);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
use Illuminate\Http\Request;
use Illuminate\Validation\Validator;
/**
* Validation rule for arithmetic captcha.
*
* Validates against session-stored answer (preferred) or input-hidden fallback.
* Expected input field: captcha (4 characters from arithmetic expression).
*/
class Captcha implements ValidationRule, ValidatorAwareRule
{
/**
* Run the validation rule.
*
* @param Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$expected = session('captcha_answer');
if ($expected === null) {
$fail('O captcha expirou. Recarregue a página.');
return;
}
if ((string) $value !== (string) $expected) {
$fail('O captcha informado está incorreto.');
}
}
/**
* Set the validator instance.
*/
public function setValidator(Validator $validator): void
{
// No-op: rule does not require validator instance.
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Services\Ai;
use App\Models\AiProvider;
use InvalidArgumentException;
class AiServiceFactory
{
/**
* Create an AI service instance based on the provider.
*
* @param AiProvider|string $provider
* @return AiServiceInterface
*/
public static function make(AiProvider|string $provider): AiServiceInterface
{
if (is_string($provider)) {
$provider = AiProvider::withTrashed()->where('slug', $provider)
->firstOrFail();
}
if (!$provider->is_active) {
throw new InvalidArgumentException('AI provider is not active.');
}
return match ($provider->slug) {
'openrouter', 'openrouter-test' => new OpenRouterService($provider),
'stepfun' => new StepFunService($provider),
'llamacpp' => new LlamaCppService($provider),
default => throw new InvalidArgumentException("Unsupported AI provider: {$provider->slug}"),
};
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Services\Ai;
interface AiServiceInterface
{
/**
* Generate a prompt using the AI provider.
*
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function generatePrompt(array $data): array;
/**
* Send a chat conversation to the AI provider.
*
* @param array<int, array{role: string, content: string}> $messages
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function chat(array $messages, array $data = []): array;
}
+73
View File
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Services\Ai;
use App\Models\AiProvider;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
class LlamaCppService implements AiServiceInterface
{
/**
* @param AiProvider $provider
*/
public function __construct(private AiProvider $provider)
{
$this->client = new Client([
'base_uri' => rtrim((string) $provider->base_url, '/') . '/',
'timeout' => 120,
]);
}
public function chat(array $messages, array $data = []): array
{
$baseUrl = rtrim((string) $this->provider->base_url, '/');
if (empty($baseUrl)) {
throw new \InvalidArgumentException('Llama.cpp base URL is not configured.');
}
try {
$response = $this->client->post('v1/chat/completions', [
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [
'model' => (string) ($data['model'] ?? ''),
'messages' => $messages,
'temperature' => isset($data['temperature']) ? (float) $data['temperature'] : null,
'max_tokens' => isset($data['max_tokens']) ? (int) $data['max_tokens'] : null,
],
]);
$body = json_decode($response->getBody()->getContents(), true);
if (!isset($body['choices'][0]['message']['content'])) {
throw new \RuntimeException('Invalid response structure from Llama.cpp server.');
}
return [
'content' => $body['choices'][0]['message']['content'],
'tokens_used' => $body['usage']['total_tokens'] ?? null,
'model' => $body['model'] ?? (string) ($data['model'] ?? ''),
];
} catch (GuzzleException $e) {
Log::error('Llama.cpp API error: ' . $e->getMessage());
throw new \RuntimeException('Failed to generate prompt with Llama.cpp: ' . $e->getMessage(), (int) $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function generatePrompt(array $data): array
{
return $this->chat(
[['role' => 'user', 'content' => (string) ($data['prompt'] ?? '')]],
$data
);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Services\Ai;
use App\Models\AiProvider;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
class OpenRouterService implements AiServiceInterface
{
/**
* @param AiProvider $provider
*/
public function __construct(private AiProvider $provider)
{
$this->client = new Client([
'base_uri' => rtrim((string) $provider->base_url, '/') . '/',
'timeout' => 120,
]);
}
public function chat(array $messages, array $data = []): array
{
$apiKey = $data['api_key'] ?? $this->provider->api_key;
if (empty($apiKey)) {
throw new \InvalidArgumentException('OpenRouter API key is not configured.');
}
try {
$response = $this->client->post('chat/completions', [
'headers' => [
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
'HTTP-Referer' => $data['http_referer'] ?? config('app.url'),
'X-Title' => $data['x_title'] ?? config('app.name'),
],
'json' => [
'model' => (string) ($data['model'] ?? ''),
'messages' => $messages,
'temperature' => isset($data['temperature']) ? (float) $data['temperature'] : null,
'max_tokens' => isset($data['max_tokens']) ? (int) $data['max_tokens'] : null,
],
]);
$body = json_decode($response->getBody()->getContents(), true);
if (!isset($body['choices'][0]['message']['content'])) {
throw new \RuntimeException('Invalid response structure from OpenRouter.');
}
return [
'content' => $body['choices'][0]['message']['content'],
'tokens_used' => $body['usage']['total_tokens'] ?? null,
'model' => $body['model'] ?? (string) ($data['model'] ?? ''),
];
} catch (GuzzleException $e) {
Log::error('OpenRouter API error: ' . $e->getMessage());
throw new \RuntimeException('Failed to generate prompt with OpenRouter: ' . $e->getMessage(), (int) $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function generatePrompt(array $data): array
{
return $this->chat(
[['role' => 'user', 'content' => (string) ($data['prompt'] ?? '')]],
$data
);
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Services\Ai;
use App\Models\AiProvider;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
class StepFunService implements AiServiceInterface
{
/**
* @param AiProvider $provider
*/
public function __construct(private AiProvider $provider)
{
$this->client = new Client([
'base_uri' => rtrim((string) $provider->base_url, '/') . '/',
'timeout' => 120,
]);
}
public function chat(array $messages, array $data = []): array
{
$apiKey = $data['api_key'] ?? $this->provider->api_key;
if (empty($apiKey)) {
throw new \InvalidArgumentException('StepFun API key is not configured.');
}
try {
$payload = [
'model' => (string) ($data['model'] ?? ''),
'messages' => $messages,
];
if (isset($data['temperature'])) {
$payload['temperature'] = (float) $data['temperature'];
}
if (isset($data['max_tokens'])) {
$payload['max_tokens'] = (int) $data['max_tokens'];
}
if (!empty($data['reasoning_effort'])) {
$payload['reasoning_effort'] = (string) $data['reasoning_effort'];
}
$response = $this->client->post('chat/completions', [
'headers' => [
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
],
'json' => $payload,
]);
$body = json_decode($response->getBody()->getContents(), true);
if (!isset($body['choices'][0]['message']['content'])) {
throw new \RuntimeException('Invalid response structure from StepFun.');
}
return [
'content' => $body['choices'][0]['message']['content'],
'tokens_used' => $body['usage']['total_tokens'] ?? null,
'model' => $body['model'] ?? (string) ($data['model'] ?? ''),
];
} catch (GuzzleException $e) {
Log::error('StepFun API error: ' . $e->getMessage());
throw new \RuntimeException('Failed to generate prompt with StepFun: ' . $e->getMessage(), (int) $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function generatePrompt(array $data): array
{
return $this->chat(
[['role' => 'user', 'content' => (string) ($data['prompt'] ?? '')]],
$data
);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\AiModel;
use App\Models\AiProvider;
use App\Services\Ai\AiServiceFactory;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class AiAgentService
{
public function __construct(
private readonly AiServiceFactory $aiServiceFactory
) {}
/**
* Execute a prompt using the specified provider and model.
*
* @return array<string, mixed>
*/
public function execute(
string $prompt,
AiProvider $provider,
AiModel $model,
float $temperature = 0.7,
int $maxTokens = 8192
): array {
$startTime = microtime(true);
$aiData = [
'prompt' => $prompt,
'temperature' => $temperature,
'max_tokens' => $maxTokens,
'model' => $model->slug,
'api_key' => $provider->api_key,
'base_url' => $provider->base_url,
];
try {
$service = $this->aiServiceFactory->make($provider);
$result = $service->generatePrompt($aiData);
} catch (ConnectionException|RuntimeException $e) {
Log::error('AI Agent execution failed: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
'content' => '',
'tokens_used' => null,
'generation_time' => round(microtime(true) - $startTime, 3),
'model' => $model->slug,
'provider' => $provider->name,
];
}
return [
'success' => true,
'content' => $result['content'] ?? '',
'tokens_used' => $result['tokens_used'] ?? null,
'generation_time' => round(microtime(true) - $startTime, 3),
'model' => $model->slug,
'provider' => $provider->name,
];
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\AiProvider;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class AiProviderService
{
/**
* Display a listing of the resource.
*
* @return array<string, mixed>
*/
public function index(): array
{
$paginator = AiProvider::with('models')
->latest()
->paginate(15);
return [
'data' => $paginator->items(),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
];
}
/**
* Store a newly created resource in storage.
*/
public function store(array $data): AiProvider
{
return AiProvider::withTrashed()->firstOrCreate(
['slug' => $data['slug']],
$data
);
}
/**
* Update the specified resource in storage.
*/
public function update(AiProvider $provider, array $data): AiProvider
{
$provider->update($data);
return $provider;
}
/**
* Remove the specified resource from storage.
*/
public function destroy(AiProvider $provider): bool
{
try {
$provider->delete();
return true;
} catch (RuntimeException $e) {
Log::error('Failed to delete AI provider: ' . $e->getMessage());
return false;
}
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\AiModel;
use App\Models\AiProvider;
use App\Services\Ai\AiServiceFactory;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class ChatService
{
private const SESSION_KEY = 'chat_conversation';
public function __construct(
private readonly AiServiceFactory $aiServiceFactory
) {}
/**
* Get conversation history from session.
*
* @return array<int, array{role: string, content: string}>
*/
public function getHistory(): array
{
return session()->get(self::SESSION_KEY, []);
}
/**
* Reset conversation history.
*/
public function reset(): void
{
session()->forget(self::SESSION_KEY);
}
/**
* Send a message and get AI response.
*
* @return array<string, mixed>
*/
public function sendMessage(
string $userMessage,
AiProvider $provider,
AiModel $model,
float $temperature = 0.7,
int $maxTokens = 8192
): array {
$history = $this->getHistory();
$history[] = ['role' => 'user', 'content' => $userMessage];
$startTime = microtime(true);
try {
$service = $this->aiServiceFactory->make($provider);
$result = $service->chat($history, [
'temperature' => $temperature,
'max_tokens' => $maxTokens,
'model' => $model->slug,
'api_key' => $provider->api_key,
'base_url' => $provider->base_url,
]);
} catch (ConnectionException|RuntimeException $e) {
Log::error('Chat API error: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
'content' => '',
'role' => 'assistant',
'tokens_used' => null,
'generation_time' => round(microtime(true) - $startTime, 3),
];
}
$assistantMessage = [
'role' => 'assistant',
'content' => $result['content'] ?? '',
];
$history[] = $assistantMessage;
session()->put(self::SESSION_KEY, $history);
return [
'success' => true,
'content' => $result['content'] ?? '',
'role' => 'assistant',
'tokens_used' => $result['tokens_used'] ?? null,
'generation_time' => round(microtime(true) - $startTime, 3),
];
}
}
+210
View File
@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\AiModel;
use App\Models\AiProvider;
use App\Models\PromptGeneration;
use App\Models\PromptProject;
use App\Models\User;
use App\Services\Ai\AiServiceFactory;
use App\Services\Ai\AiServiceInterface;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class PromptGenerationService
{
/**
* @param AiServiceFactory $factory
*/
public function __construct(private AiServiceFactory $factory)
{
}
/**
* Generate a prompt based on a prompt project.
*
* @param array<string, mixed> $data
* @return PromptGeneration
*/
public function generate(array $data): PromptGeneration
{
$project = $data['project'] ?? null;
$user = $data['user'] ?? null;
if (!$project instanceof PromptProject) {
throw new InvalidArgumentException('Prompt project is required.');
}
if (!$user instanceof User) {
$user = auth()->user();
}
if (!$user instanceof User) {
throw new InvalidArgumentException('User is required.');
}
$provider = $data['provider_id'] ?? $project->provider_id;
$model = $data['model_id'] ?? $project->model_id;
if (!$provider instanceof AiProvider) {
$provider = AiProvider::findOrFail((int) $provider);
}
if (!$model instanceof AiModel) {
$model = AiModel::findOrFail((int) $model);
}
$promptText = $this->buildPrompt($project);
$aiData = [
'prompt' => $promptText,
'temperature' => (float) ($data['temperature'] ?? $project->temperature ?? 0.7),
'max_tokens' => (int) ($data['max_tokens'] ?? $project->max_tokens ?? 1024),
'model' => $model->slug,
'api_key' => $provider->api_key,
'base_url' => $provider->base_url,
];
$startTime = microtime(true);
try {
$service = $this->factory->make($provider);
$result = $service->generatePrompt($aiData);
} catch (GuzzleException|RuntimeException $e) {
Log::error('Prompt generation failed: ' . $e->getMessage());
return PromptGeneration::create([
'project_id' => $project->id,
'user_id' => $user->id,
'provider_id' => $provider->id,
'model_id' => $model->id,
'generated_prompt' => $promptText,
'input_data' => $aiData,
'output_data' => ['error' => $e->getMessage()],
'tokens_used' => null,
'generation_time' => round(microtime(true) - $startTime, 3),
'status' => 'failed',
]);
}
$generation = PromptGeneration::create([
'project_id' => $project->id,
'user_id' => $user->id,
'provider_id' => $provider->id,
'model_id' => $model->id,
'generated_prompt' => $promptText,
'input_data' => $aiData,
'output_data' => $result,
'tokens_used' => $result['tokens_used'] ?? null,
'generation_time' => round(microtime(true) - $startTime, 3),
'status' => 'completed',
]);
return $generation;
}
/**
* Build the prompt text from project data.
*
* @param PromptProject $project
* @return string
*/
private function buildPrompt(PromptProject $project): string
{
$parts = [];
if ($project->company_name) {
$parts[] = "Company Name: " . $project->company_name;
}
if ($project->company_address) {
$parts[] = "Company Address: " . $project->company_address;
}
if ($project->company_whatsapp) {
$parts[] = "Company WhatsApp: " . $project->company_whatsapp;
}
if ($project->company_website) {
$parts[] = "Company Website: " . $project->company_website;
}
if ($project->company_social_media) {
$parts[] = "Company Social Media: " . $project->company_social_media;
}
if ($project->company_business_activities) {
$parts[] = "Company Business Activities: " . $project->company_business_activities;
}
if ($project->company_services_products) {
$parts[] = "Company Services/Products: " . $project->company_services_products;
}
if ($project->objective) {
$parts[] = "Objective: " . $project->objective;
}
if ($project->context) {
$parts[] = "Context: " . $project->context;
}
if ($project->persona) {
$parts[] = "Persona/Role: " . $project->persona;
}
if ($project->tone) {
$parts[] = "Tone/Style: " . $project->tone;
}
if ($project->constraints) {
$parts[] = "Constraints: " . $project->constraints;
}
if ($project->output_format) {
$parts[] = "Output Format: " . $project->output_format;
}
if (!empty($project->examples)) {
$parts[] = "Examples:\n" . json_encode($project->examples, JSON_PRETTY_PRINT);
}
if (!empty($project->variables)) {
$parts[] = "Variables: " . json_encode($project->variables, JSON_PRETTY_PRINT);
}
return implode("\n\n", $parts);
}
/**
* Display a listing of prompt generations.
*
* @return \Illuminate\Contracts\Pagination\Paginator
*/
public function index(): \Illuminate\Contracts\Pagination\Paginator
{
$user = auth()->user();
$query = PromptGeneration::query()
->with(['project', 'user', 'provider', 'model'])
->latest();
if (!$user || $user->role !== 'superadmin') {
$query->where('user_id', auth()->id());
}
return $query->paginate(15);
}
/**
* Delete the specified prompt generation.
*/
public function destroy(PromptGeneration $generation): void
{
$generation->delete();
}
}
+271
View File
@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\AiModel;
use App\Models\AiProvider;
use App\Models\PromptGeneration;
use App\Models\PromptProject;
use App\Services\Ai\AiServiceFactory;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
use RuntimeException;
class PromptProjectService
{
/**
* Create a new service instance.
*/
public function __construct(
private readonly AiServiceFactory $aiServiceFactory,
private readonly PromptGeneration $generationModel
) {
}
/**
* Display a listing of the resource.
*
* @return array<string, mixed>
*/
public function index(): array
{
$userId = auth()->id();
$user = auth()->user();
$query = PromptProject::with(['user', 'provider', 'model'])
->latest();
if (!$user || $user->role !== 'superadmin') {
$query->where('user_id', $userId);
}
$paginator = $query->paginate(15);
return [
'data' => $paginator->items(),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
];
}
/**
* Store a newly created resource in storage.
*/
public function store(array $data): PromptProject
{
$data['examples'] = $this->convertToArray($data['examples'] ?? null);
$data['variables'] = $this->convertToArray($data['variables'] ?? null);
return PromptProject::create($data);
}
/**
* Update the specified resource in storage.
*/
public function update(PromptProject $project, array $data): PromptProject
{
$data['examples'] = $this->convertToArray($data['examples'] ?? null);
$data['variables'] = $this->convertToArray($data['variables'] ?? null);
$project->update($data);
return $project;
}
/**
* Convert string to array.
*/
private function convertToArray(mixed $value): ?array
{
if (is_array($value)) {
return $value;
}
if (is_string($value)) {
$value = trim($value);
if ($value === '') {
return null;
}
return array_map('trim', explode(',', $value));
}
return null;
}
/**
* Remove the specified resource from storage.
*/
public function destroy(PromptProject $project): bool
{
try {
$project->delete();
return true;
} catch (RuntimeException $e) {
Log::error('Failed to delete prompt project: ' . $e->getMessage());
return false;
}
}
/**
* Generate prompt for the specified project.
*/
public function generate(PromptProject $project, mixed $model = null, ?int $userId = null): PromptGeneration
{
$provider = $project->provider;
if (!$provider instanceof AiProvider) {
throw new InvalidArgumentException('AI provider is required for generation.');
}
if (!$model) {
$model = $project->model;
}
if (!$model) {
throw new InvalidArgumentException('AI model is required for generation.');
}
if (is_string($model)) {
$model = AiModel::firstOrCreate(
['provider_id' => $provider->id, 'slug' => $model],
['name' => $model, 'slug' => $model, 'is_active' => true]
);
}
$modelSlug = $model->slug;
$modelId = $model->id;
$userId ??= auth()->id();
$promptText = $this->buildPrompt($project);
$aiData = [
'prompt' => $promptText,
'temperature' => (float) ($project->temperature ?? 0.7),
'max_tokens' => (int) ($project->max_tokens ?? 1024),
'model' => $modelSlug,
'api_key' => $provider->api_key,
'base_url' => $provider->base_url,
];
$startTime = microtime(true);
try {
$service = $this->aiServiceFactory->make($provider);
$result = $service->generatePrompt($aiData);
} catch (ConnectionException|RuntimeException $e) {
Log::error('Prompt generation failed: ' . $e->getMessage());
return $this->generationModel::create([
'project_id' => $project->id,
'user_id' => $userId,
'provider_id' => $provider->id,
'model_id' => $modelId,
'generated_prompt' => $promptText,
'input_data' => $aiData,
'output_data' => ['error' => $e->getMessage()],
'tokens_used' => null,
'generation_time' => round(microtime(true) - $startTime, 3),
'status' => 'failed',
]);
}
return $this->generationModel::create([
'project_id' => $project->id,
'user_id' => $userId,
'provider_id' => $provider->id,
'model_id' => $modelId,
'generated_prompt' => $promptText,
'input_data' => $aiData,
'output_data' => $result,
'tokens_used' => $result['tokens_used'] ?? null,
'generation_time' => round(microtime(true) - $startTime, 3),
'status' => 'completed',
]);
}
/**
* Build the prompt text from project data.
*/
public function buildPrompt(PromptProject $project): string
{
$parts = [];
$parts[] = "You are an expert prompt engineering assistant. Your task is to generate a high-quality, optimized prompt based on the specifications provided below.";
if ($project->company_name) {
$parts[] = "Company Name: " . $project->company_name;
}
if ($project->company_address) {
$parts[] = "Company Address: " . $project->company_address;
}
if ($project->company_whatsapp) {
$parts[] = "Company WhatsApp: " . $project->company_whatsapp;
}
if ($project->company_website) {
$parts[] = "Company Website: " . $project->company_website;
}
if ($project->company_social_media) {
$parts[] = "Company Social Media: " . $project->company_social_media;
}
if ($project->company_business_activities) {
$parts[] = "Company Business Activities: " . $project->company_business_activities;
}
if ($project->company_services_products) {
$parts[] = "Company Services/Products: " . $project->company_services_products;
}
if ($project->objective) {
$parts[] = "Objective: " . $project->objective;
}
if ($project->context) {
$parts[] = "Context: " . $project->context;
}
if ($project->persona) {
$parts[] = "Persona/Role: " . $project->persona;
}
if ($project->tone) {
$parts[] = "Tone/Style: " . $project->tone;
}
if ($project->constraints) {
$parts[] = "Constraints: " . $project->constraints;
}
if ($project->output_format) {
$parts[] = "Output Format: " . $project->output_format;
}
if (!empty($project->examples)) {
$parts[] = "Examples:\n" . json_encode($project->examples, JSON_PRETTY_PRINT);
}
if (!empty($project->variables)) {
$parts[] = "Variables: " . json_encode($project->variables, JSON_PRETTY_PRINT);
}
$parts[] = "Generate the optimized prompt now. Return ONLY the prompt, without explanations, introductions, or additional text.";
return implode("\n\n", $parts);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\PromptTemplate;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class PromptTemplateService
{
/**
* Display a listing of the resource.
*
* @return array<string, mixed>
*/
public function index(): array
{
$paginator = PromptTemplate::with('user')
->latest()
->paginate(15);
return [
'data' => $paginator->items(),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
];
}
/**
* Store a newly created resource in storage.
*/
public function store(array $data): PromptTemplate
{
$data['user_id'] = auth()->id();
return PromptTemplate::create($data);
}
/**
* Update the specified resource in storage.
*/
public function update(PromptTemplate $template, array $data): PromptTemplate
{
$template->update($data);
return $template;
}
/**
* Remove the specified resource from storage.
*/
public function destroy(PromptTemplate $template): bool
{
try {
$template->delete();
return true;
} catch (RuntimeException $e) {
Log::error('Failed to delete prompt template: ' . $e->getMessage());
return false;
}
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace App\Services;
class TutorialService
{
/**
* Get tutorial content.
*
* @return array<string, mixed>
*/
public function getContent(): array
{
return [
'title' => 'Prompt Engineering Tutorial',
'description' => 'Learn how to create effective prompts for AI models.',
'sections' => [
[
'id' => 'introduction',
'title' => 'Introduction',
'content' => 'Prompt engineering is the process of designing and optimizing prompts to effectively communicate with AI language models. A well-crafted prompt can significantly improve the quality and relevance of AI-generated responses.',
],
[
'id' => 'objective',
'title' => 'Objective',
'content' => 'Clearly define what you want the AI to achieve. Be specific about the goal and desired outcome. A clear objective helps the AI understand the task and produce more relevant results.',
'tips' => [
'Start with action verbs (e.g., "Write", "Analyze", "Create")',
'Specify the desired outcome explicitly',
'Keep it concise but complete',
],
],
[
'id' => 'context',
'title' => 'Context',
'content' => 'Provide background information that helps the AI understand the situation. Context includes relevant details, constraints, and situational awareness that influence the response.',
'tips' => [
'Include relevant background information',
'Mention any constraints or limitations',
'Provide examples of the desired scenario',
],
],
[
'id' => 'persona',
'title' => 'Persona / Role',
'content' => 'Define who the AI should be. Assigning a specific role or persona helps the AI adopt the appropriate tone, expertise level, and perspective for the task.',
'tips' => [
'Use professional roles (e.g., "Senior Developer", "Marketing Expert")',
'Specify expertise level (e.g., "Expert", "Beginner-friendly")',
'Combine multiple roles if needed',
],
],
[
'id' => 'tone',
'title' => 'Tone / Style',
'content' => 'Specify the communication style and tone. This ensures the AI\'s response matches your expectations for formality, friendliness, and overall style.',
'tips' => [
'Use descriptive adjectives (e.g., "Professional", "Casual", "Technical")',
'Specify formality level',
'Mention any stylistic preferences',
],
],
[
'id' => 'constraints',
'title' => 'Constraints',
'content' => 'Define limitations and rules the AI must follow. Constraints help prevent unwanted content and ensure the response meets specific requirements.',
'tips' => [
'Set word or character limits',
'Exclude certain topics or formats',
'Specify mandatory elements',
],
],
[
'id' => 'output_format',
'title' => 'Output Format',
'content' => 'Specify the expected structure of the response. A clear format helps the AI organize the information in a way that is most useful for you.',
'tips' => [
'Use markdown, JSON, or plain text',
'Specify sections and headings',
'Include examples of the desired format',
],
],
[
'id' => 'examples',
'title' => 'Examples (Few-Shot)',
'content' => 'Provide example inputs and outputs. Few-shot learning helps the AI understand the pattern and quality expected in the response.',
'tips' => [
'Include 2-3 high-quality examples',
'Show both input and desired output',
'Ensure examples cover edge cases',
],
],
[
'id' => 'variables',
'title' => 'Variables',
'content' => 'Use dynamic placeholders for reusable prompts. Variables make your prompts flexible and reusable across different scenarios.',
'tips' => [
'Use clear variable names (e.g., {{topic}}, {{audience}})',
'Document expected variable values',
'Keep variable count manageable',
],
],
[
'id' => 'temperature',
'title' => 'Temperature',
'content' => 'Control the randomness of the AI response. Lower values produce more deterministic and focused outputs, while higher values increase creativity and diversity.',
'tips' => [
'0.0 - 0.3: Factual, deterministic, focused',
'0.4 - 0.7: Balanced, natural, versatile',
'0.8 - 1.0: Creative, diverse, exploratory',
],
],
[
'id' => 'max_tokens',
'title' => 'Max Tokens',
'content' => 'Limit the length of the AI response. Setting an appropriate token limit helps control response length and cost.',
'tips' => [
'Short responses: 100-300 tokens',
'Medium responses: 500-1000 tokens',
'Long responses: 1500+ tokens',
],
],
[
'id' => 'best_practices',
'title' => 'Best Practices',
'content' => 'Follow these guidelines to create effective prompts:',
'tips' => [
'Be specific and clear about your requirements',
'Provide context and examples',
'Iterate and refine your prompts',
'Test with different models and settings',
'Keep prompts concise but complete',
'Use structured formats for complex tasks',
],
],
],
];
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.app');
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}