First Commit
This commit is contained in:
+311
@@ -0,0 +1,311 @@
|
||||
@extends("layouts.app")
|
||||
|
||||
@section("title", __("Insights"))
|
||||
|
||||
@section("header")
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ __("My Insights") }}
|
||||
</h2>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section("content")
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl 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
|
||||
|
||||
<!-- Token Usage Summary -->
|
||||
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl shadow-sm p-6 mb-6 text-white">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<p class="text-indigo-100 text-sm">{{ __("Total Input Tokens") }}</p>
|
||||
<p class="text-2xl font-bold">{{ number_format($totalInputTokens) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-indigo-100 text-sm">{{ __("Total Output Tokens") }}</p>
|
||||
<p class="text-2xl font-bold">{{ number_format($totalOutputTokens) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-indigo-100 text-sm">{{ __("Total Tokens Used") }}</p>
|
||||
<p class="text-2xl font-bold">{{ number_format($totalTokens) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generate Message -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">{{ __("Generate AI Message") }}</h3>
|
||||
<form action="{{ route("admin.insights.generate") }}" method="POST" class="space-y-4">
|
||||
@csrf
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="subject_id" class="block text-sm font-medium text-gray-700 mb-2">{{ __("Subject") }}</label>
|
||||
<select name="subject_id" id="subject_id" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
<option value="">{{ __("Select a subject...") }}</option>
|
||||
@foreach($subjects as $subject)
|
||||
<option value="{{ $subject->id }}" {{ $subjectId == $subject->id ? 'selected' : '' }}>
|
||||
{{ $subject->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="message_type" class="block text-sm font-medium text-gray-700 mb-2">{{ __("Message Type") }}</label>
|
||||
<select name="message_type" id="message_type"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
||||
@foreach(\App\Enums\MessageType::cases() as $case)
|
||||
@if(in_array($case->value, ['insight', 'language_lesson', 'game_simulator', 'technical_teaching']))
|
||||
<option value="{{ $case->value }}">{{ $case->label() }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom_prompt" class="block text-sm font-medium text-gray-700 mb-2">{{ __("Custom Prompt (optional)") }}</label>
|
||||
<input type="text" name="custom_prompt" id="custom_prompt"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="{{ __("e.g., Give me a practical tip about...") }}">
|
||||
</div>
|
||||
<div class="md:col-span-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __("Response Language") }}</label>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="language" value="pt_BR" {{ app()->getLocale() === 'pt_BR' ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
|
||||
<span class="text-sm text-gray-700">Português (Brasil)</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="language" value="en" {{ app()->getLocale() === 'en' ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
|
||||
<span class="text-sm text-gray-700">English</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="language" value="es" {{ app()->getLocale() === 'es' ? 'checked' : '' }}
|
||||
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
|
||||
<span class="text-sm text-gray-700">Español</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition">
|
||||
<svg class="w-4 h-4 mr-1.5" 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>
|
||||
{{ __("Generate Message") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="text-sm font-medium text-gray-700">{{ __("Filter by Subject") }}</label>
|
||||
<select onchange="window.location.href = '{{ route("admin.insights.index") }}?subject_id=' + this.value"
|
||||
class="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-sm">
|
||||
<option value="">{{ __("All Subjects") }}</option>
|
||||
@foreach($subjects as $subject)
|
||||
<option value="{{ $subject->id }}" {{ $subjectId == $subject->id ? 'selected' : '' }}>
|
||||
{{ $subject->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Insights List -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">{{ __("Messages History") }}</h3>
|
||||
|
||||
@forelse($insights as $insight)
|
||||
<div class="border-b border-gray-100 pb-4 mb-4 last:border-0 last:mb-0 last:pb-0">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<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 bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full">
|
||||
{{ $insight->type->label() }}
|
||||
</span>
|
||||
@if($insight->input_tokens > 0 || $insight->output_tokens > 0)
|
||||
<span class="text-xs text-gray-400">
|
||||
🔤 {{ number_format($insight->input_tokens + $insight->output_tokens) }} tokens
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-sm text-gray-900 prose prose-sm max-w-none">
|
||||
{!! \App\Services\MarkdownService::parse($insight->content) !!}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">{{ $insight->created_at->format('d/m/Y H:i') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-4">
|
||||
<form action="{{ route("admin.insights.translate", $insight) }}" method="POST" class="flex items-center gap-1">
|
||||
@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>
|
||||
<form action="{{ route("admin.insights.destroy", $insight) }}" method="POST"
|
||||
onsubmit="return confirm('{{ __("Delete this insight?") }}')">
|
||||
@csrf
|
||||
@method("DELETE")
|
||||
<button type="submit" class="text-xs text-red-500 hover:text-red-700">
|
||||
{{ __("Delete") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 text-gray-300 mx-auto mb-3" 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>
|
||||
<p class="text-gray-500">{{ __("No messages yet. Generate your first message above!") }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
@if(method_exists($insights, 'links'))
|
||||
<div class="mt-6">
|
||||
{{ $insights->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(function() {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var englishVoice = null;
|
||||
|
||||
function loadVoices() {
|
||||
var voices = speechSynthesis.getVoices();
|
||||
englishVoice = voices.find(function(v) {
|
||||
return v.lang === 'en-US' || v.lang === 'en_GB' || v.lang.startsWith('en-');
|
||||
}) || null;
|
||||
}
|
||||
|
||||
loadVoices();
|
||||
if (speechSynthesis.onvoiceschanged !== undefined) {
|
||||
speechSynthesis.onvoiceschanged = loadVoices;
|
||||
}
|
||||
|
||||
function attachAudioButtons() {
|
||||
document.querySelectorAll('.tts[data-text]:not([data-tts-attached="1"])').forEach(function(el) {
|
||||
el.dataset.ttsAttached = '1';
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.innerHTML = '🎤';
|
||||
btn.title = @json(__('Listen'));
|
||||
btn.className = 'ml-1 inline-flex items-center justify-center w-6 h-6 rounded-full bg-indigo-50 text-indigo-600 hover:bg-indigo-100 text-xs';
|
||||
|
||||
var isSpeaking = false;
|
||||
|
||||
btn.addEventListener('click', function() {
|
||||
var text = el.getAttribute('data-text') || el.textContent || '';
|
||||
if (!text.trim()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
copyFallback(text);
|
||||
return;
|
||||
}
|
||||
|
||||
var voices = speechSynthesis.getVoices();
|
||||
if (!voices || !voices.length) {
|
||||
copyFallback(text);
|
||||
return;
|
||||
}
|
||||
|
||||
speechSynthesis.cancel();
|
||||
var utter = new SpeechSynthesisUtterance(text);
|
||||
var locale = @json(app()->getLocale());
|
||||
var langMap = {
|
||||
'pt_BR': 'pt-BR',
|
||||
'en': 'en-US',
|
||||
'es': 'es-ES'
|
||||
};
|
||||
utter.lang = langMap[locale] || 'en-US';
|
||||
utter.rate = 0.9;
|
||||
if (englishVoice) {
|
||||
utter.voice = englishVoice;
|
||||
}
|
||||
utter.onerror = function(e) {
|
||||
console.error('Speech error:', e);
|
||||
copyFallback(text);
|
||||
isSpeaking = false;
|
||||
btn.classList.remove('bg-green-50', 'text-green-600');
|
||||
btn.classList.add('bg-indigo-50', 'text-indigo-600');
|
||||
};
|
||||
utter.onend = function() {
|
||||
isSpeaking = false;
|
||||
btn.classList.remove('bg-green-50', 'text-green-600');
|
||||
btn.classList.add('bg-indigo-50', 'text-indigo-600');
|
||||
};
|
||||
speechSynthesis.speak(utter);
|
||||
isSpeaking = true;
|
||||
btn.classList.remove('bg-indigo-50', 'text-indigo-600');
|
||||
btn.classList.add('bg-green-50', 'text-green-600');
|
||||
} catch (e) {
|
||||
console.error('TTS error:', e);
|
||||
copyFallback(text);
|
||||
}
|
||||
});
|
||||
|
||||
function copyFallback(text) {
|
||||
if (!text.trim()) {
|
||||
return;
|
||||
}
|
||||
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
alert(@json(__('No voices available. Text copied to listen elsewhere: ')) + text);
|
||||
}).catch(function() {
|
||||
prompt(@json(__('No voices available. Copy the text to listen:')), text);
|
||||
});
|
||||
} else {
|
||||
prompt(@json(__('No voices available. Copy the text to listen:')), text);
|
||||
}
|
||||
}
|
||||
|
||||
el.insertAdjacentElement('afterend', btn);
|
||||
});
|
||||
}
|
||||
|
||||
attachAudioButtons();
|
||||
|
||||
if (window.MutationObserver) {
|
||||
var observer = new MutationObserver(function() {
|
||||
attachAudioButtons();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
Reference in New Issue
Block a user