First Commit

This commit is contained in:
marco
2026-09-01 07:49:48 -03:00
commit 28b1044038
399 changed files with 41659 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
@extends('layouts.app')
@section('title', __('Chat with AI'))
@section('header')
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Chat with AI') }}
</h2>
<div class="flex items-center gap-4">
<x-language-selector variant="text-gray-600 hover:text-gray-800" />
</div>
</div>
@endsection
@section('content')
<div class="py-6">
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded-lg mb-6">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg mb-6">
{{ session('error') }}
</div>
@endif
<!-- Chat Container -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col" style="height: calc(100vh - 250px); min-height: 500px;">
<!-- Chat Header -->
<div class="p-4 border-b border-gray-100 flex justify-between items-center">
<div>
<h3 class="text-lg font-semibold text-gray-900">{{ __('Chat with AI') }}</h3>
<p class="text-sm text-gray-500">{{ __('Ask anything to the AI models') }}</p>
</div>
<form action="{{ route('chat.destroy') }}" method="POST" onsubmit="return confirm('{{ __("Clear chat history?") }}')">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-600 hover:text-red-800">
{{ __('Clear History') }}
</button>
</form>
</div>
<!-- Warning Banner -->
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-700">
{{ __('Do not share sensitive personal data, use profanity, or discuss prohibited topics such as pornography, politics, religion, or prejudice. Violations may result in account restrictions.') }}
</p>
</div>
</div>
</div>
<!-- Chat Messages -->
<div id="chatMessages" class="flex-1 overflow-y-auto p-4 space-y-4">
@forelse($chatHistory as $chat)
<div class="flex flex-col space-y-2">
<!-- User Message -->
<div class="flex justify-end">
<div class="bg-indigo-600 text-white rounded-lg px-4 py-2 max-w-[80%]">
<p class="text-sm whitespace-pre-wrap">{{ $chat->message }}</p>
<p class="text-xs text-indigo-200 mt-1">{{ $chat->created_at->format('H:i') }}</p>
</div>
</div>
<!-- AI Response -->
@if($chat->response)
<div class="flex justify-start">
<div class="bg-gray-100 text-gray-800 rounded-lg px-4 py-2 max-w-[80%]">
<div class="text-sm prose prose-sm max-w-none">
{!! \App\Services\MarkdownService::parse($chat->response) !!}
</div>
<div class="flex items-center gap-2 mt-1">
<p class="text-xs text-gray-400">{{ $chat->created_at->format('H:i') }}</p>
<span class="text-xs text-gray-400">|</span>
<span class="text-xs text-gray-400">{{ ucfirst($chat->provider) }}</span>
<span class="text-xs text-gray-400">|</span>
<span class="text-xs text-gray-400">{{ $chat->model }}</span>
</div>
</div>
</div>
@endif
</div>
@empty
<div class="flex items-center justify-center h-full">
<div class="text-center">
<svg class="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
<p class="text-gray-500 text-lg">{{ __('Start a conversation with AI!') }}</p>
<p class="text-gray-400 text-sm mt-2">{{ __('Type a message below to begin.') }}</p>
</div>
</div>
@endforelse
</div>
<!-- Chat Input -->
<div class="p-4 border-t border-gray-100">
<form action="{{ route('chat.store') }}" method="POST" class="flex gap-2">
@csrf
<div class="flex-1 flex gap-2">
<input type="text"
name="message"
placeholder="{{ __('Type your message...') }}"
class="flex-1 text-sm border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
autocomplete="off"
required>
<button type="submit" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
{{ __('Send') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// Auto-scroll to bottom of chat
document.addEventListener('DOMContentLoaded', function() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
});
// Focus input on load
document.addEventListener('DOMContentLoaded', function() {
const input = document.querySelector('input[name="message"]');
if (input) {
input.focus();
}
});
</script>
@endpush
+397
View File
@@ -0,0 +1,397 @@
@extends('layouts.app')
@section('title', __('Dashboard'))
@section('header')
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
<div class="flex items-center gap-4">
<x-language-selector variant="text-gray-600 hover:text-gray-800" />
</div>
</div>
@endsection
@section('content')
<div class="py-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<!-- Welcome Message -->
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl shadow-lg p-6 mb-6 text-white">
<h1 class="text-2xl font-bold">{{ __('Welcome back, :name!', ['name' => Auth::user()->name]) }}</h1>
<p class="mt-2 text-indigo-100">{{ __('Here\'s your learning overview for today.') }}</p>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded-lg mb-6">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg mb-6">
{{ session('error') }}
</div>
@endif
@if(!Auth::user()->is_approved)
<div class="bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-3 rounded-lg mb-6">
{{ __('Your account is pending approval. You will be able to access all features once approved.') }}
</div>
@endif
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">{{ __('Active Subjects') }}</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{{ $activeSubjectsCount }}</p>
</div>
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">{{ __('Total: :count subjects', ['count' => $subjectsCount]) }}</p>
</div>
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">{{ __('Total Messages') }}</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{{ number_format($insightsCount) }}</p>
</div>
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">{{ __('AI-powered learning messages') }}</p>
</div>
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">{{ __('Token Usage') }}</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{{ number_format($totalTokens) }}</p>
</div>
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
{{ __('Input: :input | Output: :output', [
'input' => number_format($totalInputTokens),
'output' => number_format($totalOutputTokens),
]) }}
</p>
@php $tokenLimit = Auth::user()->token_limit; @endphp
@if($tokenLimit)
<div class="mt-2">
<div class="flex justify-between text-xs text-gray-500 mb-1">
<span>{{ __('Limit: :limit', ['limit' => number_format($tokenLimit)]) }}</span>
<span>{{ round(($totalTokens / $tokenLimit) * 100) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="h-2 rounded-full {{ ($totalTokens / $tokenLimit) > 0.9 ? 'bg-red-500' : (($totalTokens / $tokenLimit) > 0.7 ? 'bg-yellow-500' : 'bg-blue-500') }}"
style="width: {{ min(100, ($totalTokens / $tokenLimit) * 100) }}%">
</div>
</div>
</div>
@else
<p class="text-xs text-gray-400 mt-2">{{ __('Unlimited') }}</p>
@endif
</div>
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500">{{ __('Quick Actions') }}</p>
</div>
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
</div>
<div class="mt-2 space-y-1">
<a href="{{ route('admin.subjects.index') }}" class="text-sm text-indigo-600 hover:text-indigo-800 block">
{{ __('Manage Subjects') }} →
</a>
<a href="{{ route('admin.insights.index') }}" class="text-sm text-indigo-600 hover:text-indigo-800 block">
{{ __('View Insights') }} →
</a>
<a href="{{ route('admin.telegram.index') }}" class="text-sm text-indigo-600 hover:text-indigo-800 block">
{{ __('Telegram Settings') }} →
</a>
</div>
</div>
</div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Token Usage Chart -->
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<h3 class="text-lg font-semibold text-gray-900 mb-4">{{ __('Token Usage (Last 30 Days)') }}</h3>
<div id="tokenChart" style="min-height: 300px;"></div>
</div>
<!-- Messages per Subject Chart -->
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<h3 class="text-lg font-semibold text-gray-900 mb-4">{{ __('Messages per Subject') }}</h3>
<div id="subjectChart" style="min-height: 300px;"></div>
</div>
</div>
<!-- Weekly Activity Chart -->
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100 mb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">{{ __('Weekly Activity') }}</h3>
<div id="weeklyChart" style="min-height: 200px;"></div>
</div>
<!-- Active Subjects and Recent Insights -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Active Subjects -->
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900">{{ __('Active Subjects') }}</h3>
<a href="{{ route('admin.subjects.index') }}" class="text-sm text-indigo-600 hover:text-indigo-800">
{{ __('Manage') }} →
</a>
</div>
@forelse($activeSubjects as $userSubject)
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-0">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full {{ $userSubject->is_active ? 'bg-green-500' : 'bg-gray-300' }}"></div>
<div>
<p class="text-sm font-medium text-gray-900">{{ $userSubject->subject->name }}</p>
<p class="text-xs text-gray-500">
{{ ucfirst($userSubject->frequency) }} |
{{ $userSubject->formatted_times }}
</p>
</div>
</div>
<form action="{{ route('admin.subjects.toggle', $userSubject) }}" method="POST">
@csrf
<input type="hidden" name="is_active" value="{{ $userSubject->is_active ? '0' : '1' }}">
<button type="submit" class="text-xs {{ $userSubject->is_active ? 'text-red-600 hover:text-red-800' : 'text-green-600 hover:text-green-800' }}">
{{ $userSubject->is_active ? __('Deactivate') : __('Activate') }}
</button>
</form>
</div>
@empty
<p class="text-gray-500 text-sm">{{ __('No active subjects. Subscribe to subjects to start learning!') }}</p>
<a href="{{ route('admin.subjects.index') }}" class="text-indigo-600 hover:text-indigo-800 text-sm mt-2 inline-block">
{{ __('Browse Subjects') }} →
</a>
@endforelse
</div>
<!-- Recent Messages -->
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900">{{ __('Recent Messages') }}</h3>
<a href="{{ route('admin.insights.index') }}" class="text-sm text-indigo-600 hover:text-indigo-800">
{{ __('View All') }} →
</a>
</div>
<div class="space-y-4 max-h-96 overflow-y-auto">
@forelse($recentInsights as $insight)
<div class="border-b border-gray-100 pb-3 last:border-0">
<div class="flex justify-between items-start">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-medium text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-full">
{{ $insight->subject->name }}
</span>
<span class="text-xs text-gray-400">
{{ $insight->type->label() }}
</span>
<span class="text-xs text-gray-400">
{{ $insight->created_at->diffForHumans() }}
</span>
</div>
<div class="text-sm text-gray-700 line-clamp-2 prose prose-sm max-w-none">
{!! \App\Services\MarkdownService::parse($insight->content) !!}
</div>
</div>
<form action="{{ route('admin.insights.translate', $insight) }}" method="POST" class="flex items-center gap-1 ml-2">
@csrf
<select name="target_language" class="text-xs border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-indigo-500">
<option value="pt_BR" {{ app()->getLocale() === 'pt_BR' ? 'selected' : '' }}>PT</option>
<option value="en" {{ app()->getLocale() === 'en' ? 'selected' : '' }}>EN</option>
<option value="es" {{ app()->getLocale() === 'es' ? 'selected' : '' }}>ES</option>
</select>
<button type="submit" class="text-xs text-indigo-500 hover:text-indigo-700 font-medium" title="{{ __('Translate') }}">
{{ __('Translate') }}
</button>
</form>
</div>
</div>
@empty
<p class="text-gray-500 text-sm">{{ __('No messages yet. Generate your first message!') }}</p>
<a href="{{ route('admin.insights.index') }}" class="text-indigo-600 hover:text-indigo-800 text-sm mt-2 inline-block">
{{ __('Generate Message') }} →
</a>
@endforelse
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Token Usage Chart
var tokenOptions = {
series: [
{
name: '{{ __("Input Tokens") }}',
data: @json($tokenChartData['input'] ?? [])
},
{
name: '{{ __("Output Tokens") }}',
data: @json($tokenChartData['output'] ?? [])
}
],
chart: {
type: 'area',
height: 300,
toolbar: { show: false },
fontFamily: 'Figtree, sans-serif',
foreColor: '#6B7280'
},
colors: ['#6366F1', '#8B5CF6'],
dataLabels: { enabled: false },
stroke: { curve: 'smooth', width: 2 },
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.3,
opacityTo: 0.1
}
},
xaxis: {
categories: @json($tokenChartData['labels'] ?? []),
labels: { style: { fontSize: '11px' } }
},
yaxis: {
labels: {
formatter: function(val) { return val.toLocaleString(); }
}
},
tooltip: {
y: {
formatter: function(val) { return val.toLocaleString() + ' tokens'; }
}
},
legend: { position: 'top' },
grid: { borderColor: '#f1f5f9' }
};
var tokenChart = new ApexCharts(document.querySelector("#tokenChart"), tokenOptions);
tokenChart.render();
// Subject Chart (Donut)
var subjectOptions = {
series: @json($subjectChartData['values'] ?? []),
chart: {
type: 'donut',
height: 300,
fontFamily: 'Figtree, sans-serif',
foreColor: '#6B7280'
},
labels: @json($subjectChartData['labels'] ?? []),
colors: ['#6366F1', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#3B82F6', '#EF4444'],
dataLabels: { enabled: false },
legend: {
position: 'bottom',
fontSize: '12px'
},
plotOptions: {
pie: {
donut: {
size: '65%',
labels: {
show: true,
total: {
show: true,
label: '{{ __("Total") }}',
formatter: function(w) {
return w.globals.seriesTotals.reduce((a, b) => a + b, 0).toLocaleString();
}
}
}
}
}
},
tooltip: {
y: {
formatter: function(val) { return val.toLocaleString() + ' messages'; }
}
},
responsive: [{
breakpoint: 480,
options: {
chart: { height: 250 },
legend: { position: 'bottom' }
}
}]
};
var subjectChart = new ApexCharts(document.querySelector("#subjectChart"), subjectOptions);
subjectChart.render();
// Weekly Activity Chart (Bar)
var weeklyOptions = {
series: [{
name: '{{ __("Messages") }}',
data: @json($weeklyActivity['values'] ?? [])
}],
chart: {
type: 'bar',
height: 200,
toolbar: { show: false },
fontFamily: 'Figtree, sans-serif',
foreColor: '#6B7280'
},
colors: ['#6366F1'],
plotOptions: {
bar: {
borderRadius: 4,
columnWidth: '60%',
distributed: true
}
},
dataLabels: { enabled: false },
xaxis: {
categories: @json($weeklyActivity['labels'] ?? []),
labels: { style: { fontSize: '12px' } }
},
yaxis: {
labels: {
formatter: function(val) { return Math.floor(val).toLocaleString(); }
}
},
grid: { borderColor: '#f1f5f9' },
tooltip: {
y: {
formatter: function(val) { return Math.floor(val).toLocaleString() + ' messages'; }
}
}
};
var weeklyChart = new ApexCharts(document.querySelector("#weeklyChart"), weeklyOptions);
weeklyChart.render();
});
</script>
@endpush