271 lines
10 KiB
PHP
271 lines
10 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Frontend;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\Anime;
|
||
use App\Models\Genre;
|
||
use App\Models\Analytics\AiQuery;
|
||
use App\Services\DeepSeekService;
|
||
use Illuminate\Http\Request;
|
||
|
||
class AiController extends Controller
|
||
{
|
||
/**
|
||
* AI Hub sayfası — kişisel öneri + doğal dil arama.
|
||
*/
|
||
public function index()
|
||
{
|
||
$genres = Genre::orderBy('name')->get(['id', 'name']);
|
||
return view('frontend.ai.index', compact('genres'));
|
||
}
|
||
|
||
/**
|
||
* POST /ai/chat — sohbet turu.
|
||
* Body: { messages: [{role, content}, ...] }
|
||
*/
|
||
public function chat(Request $request)
|
||
{
|
||
$ai = new DeepSeekService();
|
||
if (!$ai->isConfigured()) {
|
||
return response()->json(['error' => 'AI servisi şu an kullanılamıyor.'], 503);
|
||
}
|
||
|
||
$messages = $request->input('messages', []);
|
||
if (empty($messages)) {
|
||
return response()->json(['error' => 'Mesaj boş.'], 422);
|
||
}
|
||
|
||
// Validate structure
|
||
$messages = array_filter($messages, fn($m) => isset($m['role'], $m['content']) && in_array($m['role'], ['user', 'assistant']));
|
||
$messages = array_values($messages);
|
||
|
||
$context = $ai->getAnimeContext();
|
||
|
||
// Sayfa bağlamı — kullanıcı anime/player sayfasındaysa AI'ya söyle
|
||
$pageCtx = trim($request->input('page_context', ''));
|
||
if ($pageCtx) {
|
||
$context .= "\n\n== KULLANICI ŞU AN BU SAYFADA ==\n{$pageCtx}";
|
||
}
|
||
|
||
$rawReply = $ai->chat($messages, $context);
|
||
|
||
if (!$rawReply) {
|
||
return response()->json(['error' => 'AI yanıt vermedi, tekrar dene.'], 500);
|
||
}
|
||
|
||
// [SUGGEST:id1,id2,id3] satırını parse et
|
||
$animeCards = [];
|
||
$cleanReply = $rawReply;
|
||
if (preg_match('/\[SUGGEST:([\d,\s]+)\]\s*$/m', $rawReply, $m)) {
|
||
$cleanReply = trim(str_replace($m[0], '', $rawReply));
|
||
$ids = array_filter(array_map('intval', explode(',', $m[1])));
|
||
if ($ids) {
|
||
$animes = Anime::whereIn('id', $ids)
|
||
->where('is_published', true)
|
||
->with('genres:id,name')
|
||
->get(['id', 'title', 'slug', 'cover_image', 'rating', 'type', 'episode_count']);
|
||
$animeMap = $animes->keyBy('id');
|
||
foreach ($ids as $id) {
|
||
if ($a = $animeMap[$id] ?? null) {
|
||
$animeCards[] = [
|
||
'id' => $a->id,
|
||
'title' => $a->title,
|
||
'slug' => $a->slug,
|
||
'cover' => $a->cover_url,
|
||
'rating' => $a->rating,
|
||
'type' => $a->type,
|
||
'episode_count' => $a->episode_count,
|
||
'genres' => $a->genres->pluck('name')->take(3)->join(', '),
|
||
];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Log
|
||
$lastUser = collect($messages)->last(fn($m) => $m['role'] === 'user');
|
||
AiQuery::create(['user_id'=>auth()->id(),'query_type'=>'chat','query_text'=>substr($lastUser['content']??'',0,500),'created_at'=>now()]);
|
||
|
||
return response()->json(['reply' => $cleanReply, 'anime_cards' => $animeCards]);
|
||
}
|
||
|
||
/**
|
||
* POST /ai/recommend — kişisel öneri.
|
||
* Body: { mood?, genres[]?, type? }
|
||
*/
|
||
public function recommend(Request $request)
|
||
{
|
||
$ai = new DeepSeekService();
|
||
if (!$ai->isConfigured()) {
|
||
return response()->json(['error' => 'AI servisi şu an kullanılamıyor.'], 503);
|
||
}
|
||
|
||
$mood = trim($request->input('mood', ''));
|
||
$genres = $request->input('genres', []);
|
||
$type = $request->input('type', '');
|
||
|
||
$prefs = [];
|
||
if ($mood) $prefs[] = "Ruh hali / tema: {$mood}";
|
||
if ($genres) $prefs[] = 'Tercih edilen türler: ' . implode(', ', array_slice((array)$genres, 0, 6));
|
||
if ($type) $prefs[] = 'İçerik tipi: ' . ($type === 'movie' ? 'Film' : 'Dizi');
|
||
$prefStr = $prefs ? implode("\n", $prefs) : 'Genel tavsiye, en beğenilen animeler';
|
||
|
||
$animes = Anime::where('is_published', true)
|
||
->with('genres:id,name')
|
||
->get(['id', 'title', 'slug', 'type', 'status', 'rating', 'release_year', 'cover_image', 'episode_count']);
|
||
|
||
$result = $ai->recommend($prefStr, $animes->toArray());
|
||
|
||
if (!$result) {
|
||
return response()->json(['error' => 'Öneri üretilemedi.'], 500);
|
||
}
|
||
|
||
$animeMap = $animes->keyBy('id');
|
||
$recs = array_values(array_filter(array_map(function ($item) use ($animeMap) {
|
||
$anime = $animeMap[$item['id'] ?? 0] ?? null;
|
||
if (!$anime) return null;
|
||
return [
|
||
'id' => $anime->id,
|
||
'title' => $anime->title,
|
||
'slug' => $anime->slug,
|
||
'cover' => $anime->cover_url,
|
||
'rating' => $anime->rating,
|
||
'type' => $anime->type,
|
||
'episode_count' => $anime->episode_count,
|
||
'reason' => $item['reason'] ?? '',
|
||
];
|
||
}, $result)));
|
||
|
||
AiQuery::create(['user_id'=>auth()->id(),'query_type'=>'recommend','query_text'=>substr($prefStr,0,500),'created_at'=>now()]);
|
||
|
||
return response()->json(['recommendations' => $recs]);
|
||
}
|
||
|
||
/**
|
||
* POST /ai/search — doğal dil ile anime ara.
|
||
* Body: { query }
|
||
*/
|
||
public function search(Request $request)
|
||
{
|
||
$query = trim($request->input('query', ''));
|
||
if (!$query) {
|
||
return response()->json(['error' => 'Sorgu boş.'], 422);
|
||
}
|
||
|
||
$ai = new DeepSeekService();
|
||
if (!$ai->isConfigured()) {
|
||
return response()->json(['error' => 'AI servisi şu an kullanılamıyor.'], 503);
|
||
}
|
||
|
||
$animes = Anime::where('is_published', true)
|
||
->with('genres:id,name')
|
||
->get(['id', 'title', 'slug', 'type', 'rating', 'release_year', 'cover_image']);
|
||
|
||
$ids = $ai->naturalSearch($query, $animes->toArray());
|
||
if (!$ids) {
|
||
return response()->json(['results' => []]);
|
||
}
|
||
|
||
$animeMap = $animes->keyBy('id');
|
||
$results = array_values(array_filter(array_map(function ($id) use ($animeMap) {
|
||
$anime = $animeMap[(int)$id] ?? null;
|
||
if (!$anime) return null;
|
||
return [
|
||
'id' => $anime->id,
|
||
'title' => $anime->title,
|
||
'slug' => $anime->slug,
|
||
'cover' => $anime->cover_url,
|
||
'rating' => $anime->rating,
|
||
'type' => $anime->type,
|
||
];
|
||
}, $ids)));
|
||
|
||
AiQuery::create(['user_id'=>auth()->id(),'query_type'=>'search','query_text'=>substr($query,0,500),'created_at'=>now()]);
|
||
|
||
return response()->json(['results' => $results]);
|
||
}
|
||
|
||
/**
|
||
* POST /ai/episode-info — bölüm hakkında AI analizi.
|
||
* Body: { anime_title, episode_number, episode_title?, description? }
|
||
*/
|
||
public function episodeInfo(Request $request)
|
||
{
|
||
$ai = new DeepSeekService();
|
||
if (!$ai->isConfigured()) {
|
||
return response()->json(['error' => 'AI servisi şu an kullanılamıyor.'], 503);
|
||
}
|
||
|
||
$animeTitle = trim($request->input('anime_title', ''));
|
||
$episodeNumber = (int) $request->input('episode_number', 1);
|
||
$episodeTitle = trim($request->input('episode_title', ''));
|
||
$description = trim($request->input('description', ''));
|
||
|
||
if (!$animeTitle) {
|
||
return response()->json(['error' => 'Anime adı gerekli.'], 422);
|
||
}
|
||
|
||
$info = $ai->episodeInfo($animeTitle, $episodeNumber, $episodeTitle, $description);
|
||
if (!$info) {
|
||
return response()->json(['error' => 'Analiz yapılamadı.'], 500);
|
||
}
|
||
|
||
AiQuery::create(['user_id'=>auth()->id(),'query_type'=>'episode_info','query_text'=>"{$animeTitle} E{$episodeNumber}",'created_at'=>now()]);
|
||
|
||
return response()->json(['info' => $info]);
|
||
}
|
||
|
||
/**
|
||
* POST /ai/similar — benzer animeler.
|
||
* Body: { anime_id }
|
||
*/
|
||
public function similar(Request $request)
|
||
{
|
||
$ai = new DeepSeekService();
|
||
if (!$ai->isConfigured()) {
|
||
return response()->json(['error' => 'AI servisi şu an kullanılamıyor.'], 503);
|
||
}
|
||
|
||
$anime = Anime::with('genres:id,name')->find($request->input('anime_id'));
|
||
if (!$anime) {
|
||
return response()->json(['error' => 'Anime bulunamadı.'], 404);
|
||
}
|
||
|
||
$genres = $anime->genres->pluck('name')->join(', ');
|
||
$prefStr = "Şu anime ile benzer: {$anime->title}\n"
|
||
. "Türler: {$genres}\n"
|
||
. "Tip: " . ($anime->type === 'movie' ? 'Film' : 'Dizi') . "\n"
|
||
. "Bu animeyi beğenen izleyicilere benzer içerik öner. Aynı animeyi önerme!";
|
||
|
||
$animes = Anime::where('is_published', true)
|
||
->where('id', '!=', $anime->id)
|
||
->with('genres:id,name')
|
||
->get(['id', 'title', 'slug', 'type', 'rating', 'release_year', 'cover_image']);
|
||
|
||
$result = $ai->recommend($prefStr, $animes->toArray());
|
||
if (!$result) {
|
||
return response()->json(['similar' => []]);
|
||
}
|
||
|
||
$animeMap = $animes->keyBy('id');
|
||
$similar = array_values(array_filter(array_map(function ($item) use ($animeMap) {
|
||
$a = $animeMap[$item['id'] ?? 0] ?? null;
|
||
if (!$a) return null;
|
||
return [
|
||
'id' => $a->id,
|
||
'title' => $a->title,
|
||
'slug' => $a->slug,
|
||
'cover' => $a->cover_url,
|
||
'rating' => $a->rating,
|
||
'type' => $a->type,
|
||
'reason' => $item['reason'] ?? '',
|
||
];
|
||
}, $result)));
|
||
|
||
AiQuery::create(['user_id'=>auth()->id(),'query_type'=>'similar','query_text'=>$anime->title,'created_at'=>now()]);
|
||
|
||
return response()->json(['similar' => $similar]);
|
||
}
|
||
}
|