Files
ferrovia-regional/src/app/api/search/analyze/route.ts
T
2026-09-24 16:29:30 +02:00

221 lines
9.0 KiB
TypeScript

import { NextResponse } from 'next/server';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { supabase } from '@/lib/supabase';
import { isTrainActiveOnDate } from '@/lib/circulation';
export async function POST(req: Request) {
try {
const body = await req.json();
const { query, regionSlug, currentDate } = body;
if (!query) {
return NextResponse.json({ error: "No query provided" }, { status: 400 });
}
let parsedIntent: any = { intent: 'unknown' };
const apiKey = process.env.GEMINI_API_KEY;
if (apiKey) {
try {
const genAI = new GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
const prompt = `
Tu es un assistant de recherche pour un site régional de trains (TER).
Date actuelle : ${currentDate}.
L'utilisateur recherche : "${query}"
Analyse l'intention de l'utilisateur et retourne UNIQUEMENT un objet JSON avec cette structure :
{
"intent": "train" | "itineraire" | "fiche_horaire" | "gare" | "unknown",
"trainNumber": "numéro à 6 chiffres (ex: 891811) s'il cherche un train précis",
"date": "YYYY-MM-DD s'il précise une date ou un jour (ex: demain, lundi, 24 sept), sinon null",
"departureStation": "nom de la gare de départ s'il cherche un itinéraire",
"arrivalStation": "nom de la gare d'arrivée s'il cherche un itinéraire",
"station": "nom de la gare s'il cherche une gare ou ses fiches horaires",
"line": "nom de la ligne (ex: Dijon Besançon) s'il cherche une fiche horaire de ligne"
}
Règles :
- "train 891811 demain" -> intent: train, trainNumber: "891811", date: [date de demain]
- "horaires dijon besancon" ou "itinéraire de dijon à seurre" -> intent: itineraire, departureStation: "Dijon", arrivalStation: "Besancon" (ou "Seurre")
- "fiches horaires beaune" -> intent: fiche_horaire, station: "Beaune"
- "gare de dijon" -> intent: gare, station: "Dijon"
`;
const result = await model.generateContent(prompt);
const text = result.response.text();
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
parsedIntent = JSON.parse(jsonMatch[0]);
}
} catch (err) {
console.error("AI Error:", err);
}
}
// Fallback if AI fails or no API key
if (parsedIntent.intent === 'unknown') {
const q = query.toLowerCase();
const numMatch = q.match(/(\d{5,6})/);
let fallbackDate = null;
const days = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const foundDay = days.find(d => q.includes(d));
if (q.includes('demain')) {
const d = new Date(currentDate);
d.setDate(d.getDate() + 1);
fallbackDate = d.toISOString().split('T')[0];
} else if (q.includes('aujourd\'hui')) {
fallbackDate = currentDate.split('T')[0];
} else if (foundDay) {
const currentDay = new Date(currentDate).getDay();
const targetDay = days.indexOf(foundDay);
let diff = targetDay - currentDay;
if (diff < 0) diff += 7; // next occurrence
if (diff === 0 && q.includes('prochain')) diff += 7;
const targetDate = new Date(currentDate);
targetDate.setDate(targetDate.getDate() + diff);
fallbackDate = targetDate.toISOString().split('T')[0];
}
if (numMatch) {
parsedIntent = { intent: 'train', trainNumber: numMatch[1], date: fallbackDate };
} else if (q.includes('itineraire') || q.includes('itinéraire') || q.includes('trajet')) {
const parts = q.replace(/(itinéraire|itineraire|trajet)s? (de )?/i, '').split(/ à | vers /);
if (parts.length === 2) {
parsedIntent = { intent: 'itineraire', departureStation: parts[0].trim(), arrivalStation: parts[1].trim() };
}
} else if (q.includes('fiche') || q.includes('horaire')) {
if (q.includes('gare')) {
parsedIntent = { intent: 'fiche_horaire', station: q.replace(/fiches? horaires?( de la)? gare( de)?/i, '').trim() };
} else {
const parts = q.split(/ à | vers /);
if (parts.length === 2) {
parsedIntent = { intent: 'itineraire', departureStation: parts[0].replace(/horaires?( de )?/i, '').trim(), arrivalStation: parts[1].trim() };
}
}
} else if (q.includes('gare')) {
parsedIntent = { intent: 'gare', station: q.replace(/gares?( de)?/i, '').trim() };
} else {
// Ultimate fallback: "Dijon à Seurre"
const parts = q.split(/ à | vers /);
if (parts.length === 2) {
parsedIntent = { intent: 'itineraire', departureStation: parts[0].trim(), arrivalStation: parts[1].trim() };
}
}
}
const todayIso = new Date(currentDate).toISOString().split('T')[0];
const targetIsoDate = parsedIntent.date || todayIso;
const targetDateFR = new Date(targetIsoDate).toLocaleDateString('fr-FR');
let actionResponse: any = { type: 'unknown', message: 'Je n\'ai pas bien compris votre recherche.' };
if (parsedIntent.intent === 'train' && parsedIntent.trainNumber) {
const { data: trains } = await supabase
.from('horaires')
.select('*')
.eq('numero_train', parsedIntent.trainNumber)
.eq('actif', true)
.is('deleted_at', null);
if (!trains || trains.length === 0) {
actionResponse = {
type: 'error',
message: `Désolé, nous n'avons pas trouvé le train n°${parsedIntent.trainNumber} dans notre base de données.`
};
} else {
const isActive = trains.some(train => isTrainActiveOnDate(train, targetIsoDate));
if (!isActive) {
actionResponse = {
type: 'error',
message: `Le train ${parsedIntent.trainNumber} ne circule pas le ${targetDateFR}.`
};
} else {
const params = new URLSearchParams();
if (parsedIntent.date) {
const parts = parsedIntent.date.split('-');
if (parts.length === 3) {
params.set('date', `${parts[2]}/${parts[1]}/${parts[0]}`);
}
}
const queryString = params.toString() ? `?${params.toString()}` : '';
actionResponse = {
type: 'redirect',
url: `/${regionSlug}/horaires/train/${parsedIntent.trainNumber}${queryString}`,
message: `Je vous redirige vers les détails du train ${parsedIntent.trainNumber} pour le ${targetDateFR}.`
};
}
}
} else if (parsedIntent.intent === 'itineraire' && parsedIntent.departureStation && parsedIntent.arrivalStation) {
const params = new URLSearchParams();
params.set('departure', parsedIntent.departureStation);
params.set('arrival', parsedIntent.arrivalStation);
if (parsedIntent.date) {
const parts = parsedIntent.date.split('-');
if (parts.length === 3) {
params.set('outwardDate', `Lun. ${parts[2]}/${parts[1]}`); // basic format
}
}
actionResponse = {
type: 'redirect',
url: `/${regionSlug}/horaires?${params.toString()}`,
message: `Recherche de l'itinéraire de ${parsedIntent.departureStation} à ${parsedIntent.arrivalStation}...`
};
} else if (parsedIntent.intent === 'fiche_horaire') {
const params = new URLSearchParams();
if (parsedIntent.station) params.set('gare', parsedIntent.station);
if (parsedIntent.line) params.set('ligne', parsedIntent.line);
actionResponse = {
type: 'redirect',
url: `/${regionSlug}/se-deplacer/fiches-horaires?${params.toString()}`,
message: `Ouverture des fiches horaires...`
};
} else if (parsedIntent.intent === 'gare' && parsedIntent.station) {
const searchName = parsedIntent.station;
const { data: gares } = await supabase
.from('gares')
.select('nom')
.ilike('nom', `%${searchName}%`)
.is('deleted_at', null)
.order('nom')
.limit(5);
if (!gares || gares.length === 0) {
actionResponse = {
type: 'error',
message: `Désolé, nous n'avons trouvé aucune gare correspondant à "${searchName}".`
};
} else if (gares.length === 1 && gares[0].nom.toLowerCase() === searchName.toLowerCase()) {
actionResponse = {
type: 'redirect',
url: `/${regionSlug}/se-deplacer/gares?gare=${encodeURIComponent(gares[0].nom)}`,
message: `Ouverture de la page de la gare de ${gares[0].nom}...`
};
} else {
// Multiple matches or partial match
actionResponse = {
type: 'options',
message: `Plusieurs gares correspondent à votre recherche. Laquelle cherchez-vous ?`,
options: gares.map(g => ({
title: `Gare de ${g.nom}`,
url: `/${regionSlug}/se-deplacer/gares?gare=${encodeURIComponent(g.nom)}`,
icon: 'place'
}))
};
}
}
return NextResponse.json(actionResponse);
} catch (error) {
console.error(error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}