221 lines
9.0 KiB
TypeScript
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 });
|
|
}
|
|
}
|