Ajout de HelloFresh
This commit is contained in:
185
Controllers/AccountController.cs
Normal file
185
Controllers/AccountController.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using administration.Models.Finances;
|
||||
using administration.Models;
|
||||
using User = administration.Models.User;
|
||||
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly LayoutDataContext _db;
|
||||
private readonly IPasswordHasher<User> _hasher;
|
||||
|
||||
public AccountController(LayoutDataContext db, IPasswordHasher<User> hasher)
|
||||
{
|
||||
_db = db;
|
||||
_hasher = hasher;
|
||||
}
|
||||
|
||||
// GET /Account/Login
|
||||
[HttpGet]
|
||||
public IActionResult Login(string? returnUrl = null)
|
||||
{
|
||||
ViewBag.ReturnUrl = returnUrl;
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST /Account/Login
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(string username, string password, string? returnUrl = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
ModelState.AddModelError("", "Identifiants requis.");
|
||||
return View();
|
||||
}
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Username == username);
|
||||
if (user == null)
|
||||
{
|
||||
ModelState.AddModelError("", "Identifiants invalides.");
|
||||
return View();
|
||||
}
|
||||
|
||||
var verify = _hasher.VerifyHashedPassword(user, user.PasswordHash, password);
|
||||
if (verify == PasswordVerificationResult.Failed)
|
||||
{
|
||||
ModelState.AddModelError("", "Identifiants invalides.");
|
||||
return View();
|
||||
}
|
||||
|
||||
// Claims pour cookie d’auth
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new Claim(ClaimTypes.Name, user.Username)
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
|
||||
|
||||
// Compatibilité avec ton code existant
|
||||
HttpContext.Session.SetInt32("UserId", user.Id);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(returnUrl) && Url.IsLocalUrl(returnUrl))
|
||||
return Redirect(returnUrl);
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
// POST /Account/Logout
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await HttpContext.SignOutAsync();
|
||||
HttpContext.Session.Clear();
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
// GET /Account/ForgotPassword
|
||||
[HttpGet]
|
||||
public IActionResult ForgotPassword() => View();
|
||||
|
||||
// POST /Account/ForgotPassword
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ForgotPassword(string usernameOrEmail)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(usernameOrEmail))
|
||||
{
|
||||
ModelState.AddModelError("", "Champ requis.");
|
||||
return View();
|
||||
}
|
||||
|
||||
var user = await _db.Users
|
||||
.FirstOrDefaultAsync(u => u.Username == usernameOrEmail);
|
||||
|
||||
// Par sécurité, on ne révèle pas si l’utilisateur existe
|
||||
if (user != null)
|
||||
{
|
||||
// Génère un token simple (tu peux le hasher si tu veux)
|
||||
var token = Convert.ToBase64String(Guid.NewGuid().ToByteArray())
|
||||
.Replace("+", "-").Replace("/", "_").TrimEnd('=');
|
||||
|
||||
user.ResetToken = token;
|
||||
user.ResetTokenExpiresAt = DateTimeOffset.UtcNow.AddHours(1);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var resetUrl = Url.Action("ResetPassword", "Account", new { token = token }, Request.Scheme);
|
||||
// TODO: envoyer resetUrl par email.
|
||||
// Temporaire : on l’affiche
|
||||
TempData["ResetLink"] = resetUrl;
|
||||
}
|
||||
|
||||
TempData["Info"] = "Si un compte existe, un lien de réinitialisation a été envoyé.";
|
||||
return RedirectToAction("ForgotPassword");
|
||||
}
|
||||
|
||||
// GET /Account/ResetPassword?token=...
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ResetPassword(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token)) return RedirectToAction("Login");
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u =>
|
||||
u.ResetToken == token && u.ResetTokenExpiresAt > DateTimeOffset.UtcNow);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
TempData["Error"] = "Lien invalide ou expiré.";
|
||||
return RedirectToAction("ForgotPassword");
|
||||
}
|
||||
|
||||
ViewBag.Token = token;
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST /Account/ResetPassword
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetPassword(string token, string newPassword, string confirmPassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
TempData["Error"] = "Token manquant.";
|
||||
return RedirectToAction("ForgotPassword");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(newPassword) || newPassword != confirmPassword)
|
||||
{
|
||||
ModelState.AddModelError("", "Les mots de passe ne correspondent pas.");
|
||||
ViewBag.Token = token;
|
||||
return View();
|
||||
}
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u =>
|
||||
u.ResetToken == token && u.ResetTokenExpiresAt > DateTimeOffset.UtcNow);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
TempData["Error"] = "Lien invalide ou expiré.";
|
||||
return RedirectToAction("ForgotPassword");
|
||||
}
|
||||
|
||||
user.PasswordHash = _hasher.HashPassword(user, newPassword);
|
||||
user.ResetToken = null;
|
||||
user.ResetTokenExpiresAt = null;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
TempData["Ok"] = "Mot de passe réinitialisé. Connecte-toi.";
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
// (Optionnel) création rapide d’un compte admin si tu n’as rien en BDD
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> SeedAdmin(string username = "admin", string password = "ChangeMe!123")
|
||||
{
|
||||
if (await _db.Users.AnyAsync()) return BadRequest("Déjà des utilisateurs en BDD.");
|
||||
var u = new User { Username = username };
|
||||
u.PasswordHash = _hasher.HashPassword(u, password);
|
||||
_db.Users.Add(u);
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok("Admin créé.");
|
||||
}
|
||||
}
|
||||
16
Controllers/AppController.cs
Normal file
16
Controllers/AppController.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
public abstract class AppController : Controller
|
||||
{
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var uid = HttpContext.Session.GetInt32("UserId");
|
||||
var uname = HttpContext.Session.GetString("UserName");
|
||||
|
||||
ViewBag.UserId = uid;
|
||||
ViewBag.UserName = uname;
|
||||
|
||||
base.OnActionExecuting(context);
|
||||
}
|
||||
}
|
||||
108
Controllers/ConnectionsController.cs
Normal file
108
Controllers/ConnectionsController.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Security.Claims;
|
||||
using administration.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace administration.Controllers
|
||||
{
|
||||
public class ConnectionsController : Controller
|
||||
{
|
||||
private readonly LayoutDataContext _db;
|
||||
private readonly IPasswordHasher<User> _hasher;
|
||||
|
||||
public ConnectionsController(LayoutDataContext db, IPasswordHasher<User> hasher)
|
||||
{
|
||||
_db = db;
|
||||
_hasher = hasher;
|
||||
}
|
||||
|
||||
// --- PAGE DE LOGIN ---
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Login() => View();
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(string username, string password, string? returnUrl = null)
|
||||
{
|
||||
// 1) Recherche de l'utilisateur
|
||||
var user = _db.Users.FirstOrDefault(u => u.Username == username);
|
||||
if (user == null)
|
||||
{
|
||||
ViewBag.Error = "Nom d’utilisateur ou mot de passe incorrect.";
|
||||
return View();
|
||||
}
|
||||
|
||||
// 2) Vérification du mot de passe
|
||||
var verification = _hasher.VerifyHashedPassword(user, user.PasswordHash ?? "", password);
|
||||
if (verification != PasswordVerificationResult.Success &&
|
||||
verification != PasswordVerificationResult.SuccessRehashNeeded)
|
||||
{
|
||||
ViewBag.Error = "Nom d’utilisateur ou mot de passe incorrect.";
|
||||
return View();
|
||||
}
|
||||
|
||||
// 3) Si rehash nécessaire → on sauvegarde
|
||||
if (verification == PasswordVerificationResult.SuccessRehashNeeded)
|
||||
{
|
||||
user.PasswordHash = _hasher.HashPassword(user, password);
|
||||
_db.SaveChanges();
|
||||
}
|
||||
|
||||
// 4) Sauvegarde dans la session (si besoin pour ton code existant)
|
||||
HttpContext.Session.SetInt32("UserId", user.Id);
|
||||
HttpContext.Session.SetString("UserName", user.Username);
|
||||
|
||||
// 5) Création du cookie d'authentification
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new Claim(ClaimTypes.Name, user.Username)
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
await HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(identity));
|
||||
|
||||
// 6) Redirection sécurisée
|
||||
if (!string.IsNullOrWhiteSpace(returnUrl) &&
|
||||
Url.IsLocalUrl(returnUrl) &&
|
||||
!returnUrl.Contains("/Connections/Login", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
// --- DECONNEXION ---
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await HttpContext.SignOutAsync(); // supprime le cookie
|
||||
HttpContext.Session.Clear(); // vide la session
|
||||
return RedirectToAction("Login", "Connections");
|
||||
}
|
||||
|
||||
// --- MISE À JOUR DU MOT DE PASSE ---
|
||||
// ⚠️ On met [Authorize] pour éviter un accès public
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public IActionResult SetPassword(string username, string newPassword)
|
||||
{
|
||||
var user = _db.Users.FirstOrDefault(u => u.Username == username);
|
||||
if (user == null)
|
||||
return Content("❌ Utilisateur introuvable");
|
||||
|
||||
user.PasswordHash = _hasher.HashPassword(user, newPassword);
|
||||
_db.SaveChanges();
|
||||
|
||||
return Content("✅ Mot de passe mis à jour avec succès");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
using administration.Models;
|
||||
using administration.Models.Finances;
|
||||
using administration.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace administration.Controllers
|
||||
namespace administration.Controllers.Finances
|
||||
{
|
||||
/// <summary>
|
||||
/// Contrôleur API pour la gestion des dépenses de l'utilisateur.
|
||||
@@ -13,6 +13,7 @@ namespace administration.Controllers
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class ExpenseController : Controller
|
||||
{
|
||||
private readonly FinancesContext _context;
|
||||
@@ -112,6 +113,29 @@ namespace administration.Controllers
|
||||
return StatusCode(500, $"Erreur serveur : {ex.Message}, Méthode GetAdditionalRevenues");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Récupère les dépenses depuis la base de données avec l'id X.
|
||||
/// </summary>
|
||||
/// <param name="rows">L'id de la dépense</param>
|
||||
/// <returns>La bonne dépense</returns>
|
||||
[HttpGet("expense_by_id")]
|
||||
public IActionResult GetExpenseByID(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
Expense expense = _context.Expenses
|
||||
.Where(r => r.Id == id)
|
||||
.FirstOrDefault();
|
||||
|
||||
return Ok(expense);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, $"Erreur serveur : {ex.Message}, Méthode GetRevenueByID");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using administration.Models;
|
||||
using administration.Services;
|
||||
using administration.Models.Finances;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace administration.Controllers
|
||||
namespace administration.Controllers.Finances
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class RevenueController : ControllerBase
|
||||
[Authorize]
|
||||
public class FinancesController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
private readonly FinancesContext _context;
|
||||
private readonly IUserSessionService _userSession;
|
||||
|
||||
public RevenueController(FinancesContext context, IUserSessionService userSession)
|
||||
public FinancesController(FinancesContext context, IUserSessionService userSession)
|
||||
{
|
||||
_context = context;
|
||||
_userSession = userSession;
|
||||
@@ -122,5 +126,28 @@ namespace administration.Controllers
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Récupère les revenues depuis la base de données avec l'id X.
|
||||
/// </summary>
|
||||
/// <param name="rows">L'id du revenue</param>
|
||||
/// <returns>Le bon revenue</returns>
|
||||
[HttpGet("revenues_by_id")]
|
||||
public IActionResult GetRevenueByID(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
Revenue revenue = _context.Revenues
|
||||
.Where(r => r.Id == id)
|
||||
.FirstOrDefault();
|
||||
|
||||
return Ok(revenue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, $"Erreur serveur : {ex.Message}, Méthode GetRevenueByID");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
1109
Controllers/HelloFresh/HelloFreshController.cs
Normal file
1109
Controllers/HelloFresh/HelloFreshController.cs
Normal file
File diff suppressed because it is too large
Load Diff
138
Controllers/HelloFresh/HelloFreshScraperService.cs
Normal file
138
Controllers/HelloFresh/HelloFreshScraperService.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace HelloFreshScraper.Services
|
||||
{
|
||||
public class Recipe
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Image { get; set; }
|
||||
public string Pdf { get; set; }
|
||||
public string PrepTime { get; set; } // ex: "30-35 min"
|
||||
public string Difficulty { get; set; } // ex: "Intermédiaire"
|
||||
public string Description { get; set; } // ex: "Accompagné de..."
|
||||
public string Label { get; set; }
|
||||
}
|
||||
|
||||
public class HelloFreshScraperService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IBrowsingContext _context;
|
||||
|
||||
public HelloFreshScraperService(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
_context = BrowsingContext.New(config);
|
||||
}
|
||||
|
||||
public async Task<List<Recipe>> GetRecipesAsync(string locale = "fr-fr", int startPage = 1, int pagesToLoad = 2)
|
||||
{
|
||||
var recipesDict = new Dictionary<string, Recipe>();
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
var context = BrowsingContext.New(config);
|
||||
|
||||
for (int page = startPage; page < startPage + pagesToLoad; page++)
|
||||
{
|
||||
var url = $"https://hfresh.info/{locale}?page={page}";
|
||||
var html = await _httpClient.GetStringAsync(url);
|
||||
|
||||
var document = await context.OpenAsync(req => req.Content(html));
|
||||
var rawData = document.QuerySelector("#app")?.GetAttribute("data-page");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawData)) continue;
|
||||
|
||||
var parsed = JObject.Parse(rawData);
|
||||
var recipeArray = parsed.SelectToken("props.recipes.data") as JArray;
|
||||
|
||||
foreach (var item in recipeArray ?? new JArray())
|
||||
{
|
||||
var id = item["id"]?.ToString();
|
||||
var name = item["name"]?.ToString();
|
||||
var pdf = item["pdf"]?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(pdf))
|
||||
continue;
|
||||
|
||||
if (recipesDict.ContainsKey(id)) continue; // éviter les doublons
|
||||
|
||||
var nameKey = name.ToLower().Trim();
|
||||
if (recipesDict.Values.Any(r => r.Name.ToLower().Trim() == nameKey)) continue;
|
||||
|
||||
|
||||
var recipe = new Recipe
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Image = item["image"]?.ToString(),
|
||||
Pdf = pdf,
|
||||
Description = item["headline"]?.ToString(),
|
||||
Label = item["label"]?.ToString()
|
||||
};
|
||||
|
||||
recipesDict[id] = recipe;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Scrape en parallèle les pages de détail
|
||||
var tasks = recipesDict.Values.Select(async recipe =>
|
||||
{
|
||||
var slug = GenerateSlug(recipe.Name);
|
||||
var url = $"https://www.hellofresh.fr/recipes/{slug}-{recipe.Id}";
|
||||
|
||||
try
|
||||
{
|
||||
var html = await _httpClient.GetStringAsync(url);
|
||||
|
||||
// PrepTime (Regex simple, ex: "35 minutes")
|
||||
var match = Regex.Match(html, @"(\d{1,3})\s*minutes?", RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
recipe.PrepTime = match.Groups[1].Value;
|
||||
|
||||
// Difficulty
|
||||
var diffMatch = Regex.Match(html, @"<span[^>]*data-translation-id=[""']recipe-detail\.level-number[^>]*>([^<]+)</span>", RegexOptions.IgnoreCase);
|
||||
if (diffMatch.Success)
|
||||
recipe.Difficulty = diffMatch.Groups[1].Value.Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ Erreur scraping {url} : {ex.Message}");
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks); // 🧠 Attendre que tous les détails soient récupérés
|
||||
|
||||
return recipesDict.Values.ToList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private string GenerateSlug(string name)
|
||||
{
|
||||
var slug = name.ToLower()
|
||||
.Replace("é", "e").Replace("è", "e").Replace("ê", "e")
|
||||
.Replace("à", "a").Replace("â", "a").Replace("ù", "u")
|
||||
.Replace("î", "i").Replace("ô", "o").Replace("ç", "c")
|
||||
.Replace("œ", "oe").Replace("&", "et")
|
||||
.Replace("’", "-").Replace("'", "-")
|
||||
.Replace("\"", "").Replace(",", "").Replace(":", "")
|
||||
.Replace("!", "").Replace("?", "").Replace("(", "").Replace(")", "")
|
||||
.Replace(" ", " ").Replace(" ", "-");
|
||||
|
||||
while (slug.Contains("--"))
|
||||
slug = slug.Replace("--", "-");
|
||||
|
||||
return slug.Trim('-');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,63 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using administration.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace administration.Controllers;
|
||||
|
||||
public class HomeController : Controller
|
||||
namespace administration.Controllers
|
||||
{
|
||||
private readonly FinancesContext _context;
|
||||
|
||||
public HomeController(FinancesContext context)
|
||||
[Authorize] // toute la classe nécessite une connexion
|
||||
public class HomeController : Controller
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
private readonly ILogger<HomeController> _logger;
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
// R<>cup<75>re un utilisateur fictif pour l<>exemple
|
||||
var user = _context.Users.Where(X => X.Id == 2).First(); // <20> remplacer par un filtre r<>el (par ex. Email ou Id)
|
||||
|
||||
if (user != null)
|
||||
public HomeController(ILogger<HomeController> logger)
|
||||
{
|
||||
// Stocker dans la session
|
||||
HttpContext.Session.SetInt32("UserId", user.Id);
|
||||
HttpContext.Session.SetString("UserName", user.Name);
|
||||
_logger = logger;
|
||||
}
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult Profile()
|
||||
{
|
||||
int? userId = HttpContext.Session.GetInt32("UserId");
|
||||
string userName = HttpContext.Session.GetString("UserName");
|
||||
public IActionResult Index()
|
||||
{
|
||||
EnsureSessionFromClaims();
|
||||
return View();
|
||||
}
|
||||
|
||||
ViewBag.UserId = userId;
|
||||
ViewBag.UserName = userName;
|
||||
public IActionResult Profile()
|
||||
{
|
||||
EnsureSessionFromClaims();
|
||||
|
||||
return View();
|
||||
}
|
||||
ViewBag.UserId = HttpContext.Session.GetInt32("UserId");
|
||||
ViewBag.UserName = HttpContext.Session.GetString("UserName");
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
return View();
|
||||
}
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Si la session est vide, recharge UserId et UserName depuis les claims
|
||||
/// </summary>
|
||||
private void EnsureSessionFromClaims()
|
||||
{
|
||||
if (!HttpContext.Session.Keys.Contains("UserId") || !HttpContext.Session.Keys.Contains("UserName"))
|
||||
{
|
||||
var userIdClaim = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var userNameClaim = User.FindFirstValue(ClaimTypes.Name);
|
||||
|
||||
if (!string.IsNullOrEmpty(userIdClaim) && int.TryParse(userIdClaim, out int userId))
|
||||
{
|
||||
HttpContext.Session.SetInt32("UserId", userId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(userNameClaim))
|
||||
{
|
||||
HttpContext.Session.SetString("UserName", userNameClaim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
Controllers/OllamaController.cs
Normal file
31
Controllers/OllamaController.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using IngredientsAI.Services;
|
||||
|
||||
namespace IngredientsAI.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class OllamaController : ControllerBase
|
||||
{
|
||||
private readonly OllamaService _ollama;
|
||||
|
||||
public OllamaController(OllamaService ollama)
|
||||
{
|
||||
_ollama = ollama;
|
||||
}
|
||||
|
||||
[HttpGet("models")]
|
||||
public async Task<IActionResult> GetModels()
|
||||
{
|
||||
try
|
||||
{
|
||||
var models = await _ollama.GetModelsAsync();
|
||||
return Ok(new { items = models });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Controllers/OllamaService.cs
Normal file
43
Controllers/OllamaService.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace IngredientsAI.Services
|
||||
{
|
||||
public class OllamaService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public OllamaService(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
// Ignore SSL errors (utile pour ton VPS si certificat pas reconnu en local)
|
||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
};
|
||||
|
||||
_http = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri(_config["Ollama:Url"] ?? "https://ollama.byakurepo.online")
|
||||
};
|
||||
}
|
||||
|
||||
// Exemple : récupère la liste des modèles disponibles
|
||||
public async Task<IEnumerable<string>> GetModelsAsync()
|
||||
{
|
||||
var res = await _http.GetAsync("/api/tags");
|
||||
res.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await res.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
|
||||
return doc.RootElement
|
||||
.GetProperty("models")
|
||||
.EnumerateArray()
|
||||
.Select(m => m.GetProperty("name").GetString() ?? string.Empty)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using System.IO;
|
||||
|
||||
namespace administration.Controllers
|
||||
{
|
||||
public class PagesController : Controller
|
||||
{
|
||||
private readonly ICompositeViewEngine _viewEngine;
|
||||
private readonly ITempDataProvider _tempDataProvider;
|
||||
|
||||
public PagesController(ICompositeViewEngine viewEngine, ITempDataProvider tempDataProvider)
|
||||
{
|
||||
_viewEngine = viewEngine;
|
||||
_tempDataProvider = tempDataProvider;
|
||||
}
|
||||
|
||||
[HttpGet("/pages/{folder}/{page}")]
|
||||
public async Task<IActionResult> Render(string folder, string page)
|
||||
{
|
||||
var allowedFolders = new[] { "Charts", "Components", "Connections", "Home", "Tables", "Utilities" };
|
||||
|
||||
var realFolder = allowedFolders.FirstOrDefault(f => string.Equals(f, folder, StringComparison.OrdinalIgnoreCase));
|
||||
if (realFolder == null)
|
||||
return Content("Ce dossier n'est pas autorisé.", "text/plain; charset=utf-8");
|
||||
|
||||
// Trouver la vraie casse du fichier sur disque
|
||||
var folderPath = Path.Combine(Directory.GetCurrentDirectory(), "Views", realFolder);
|
||||
var cshtmlFile = Directory.GetFiles(folderPath, "*.cshtml")
|
||||
.FirstOrDefault(f => string.Equals(Path.GetFileNameWithoutExtension(f), page, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (cshtmlFile == null)
|
||||
return NotFound($"La vue ~/Views/{realFolder}/{page}.cshtml n'existe pas physiquement.");
|
||||
|
||||
var razorViewPath = $"~/Views/{realFolder}/{Path.GetFileName(cshtmlFile)}";
|
||||
var result = _viewEngine.GetView(null, razorViewPath, true);
|
||||
|
||||
if (!result.Success)
|
||||
return NotFound($"Vue Razor non trouvée : {razorViewPath}");
|
||||
|
||||
|
||||
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
|
||||
var tempData = new TempDataDictionary(HttpContext, _tempDataProvider);
|
||||
|
||||
using var sw = new StringWriter();
|
||||
var context = new ViewContext(
|
||||
ControllerContext,
|
||||
result.View,
|
||||
viewData,
|
||||
tempData,
|
||||
sw,
|
||||
new HtmlHelperOptions()
|
||||
);
|
||||
|
||||
await result.View.RenderAsync(context);
|
||||
return Content(sw.ToString(), "text/html");
|
||||
}
|
||||
}
|
||||
}
|
||||
504
Controllers/SearchController.cs
Normal file
504
Controllers/SearchController.cs
Normal file
@@ -0,0 +1,504 @@
|
||||
//// Controllers/SearchController.cs
|
||||
//using Microsoft.AspNetCore.Mvc;
|
||||
//using Microsoft.Extensions.Logging;
|
||||
//using System.Net.Http.Headers;
|
||||
//using System.Text;
|
||||
//using System.Text.Json;
|
||||
//using System.Text.Json.Serialization;
|
||||
//using System.Text.RegularExpressions;
|
||||
|
||||
//namespace Administration.Controllers
|
||||
//{
|
||||
// [ApiController]
|
||||
// [Route("api/ai")]
|
||||
// public class SearchController : ControllerBase
|
||||
// {
|
||||
// private readonly IHttpClientFactory _httpFactory;
|
||||
// private readonly IConfiguration _cfg;
|
||||
// private readonly ILogger<SearchController> _log;
|
||||
|
||||
// public SearchController(IHttpClientFactory httpFactory, IConfiguration cfg, ILogger<SearchController> log)
|
||||
// {
|
||||
// _httpFactory = httpFactory;
|
||||
// _cfg = cfg;
|
||||
// _log = log;
|
||||
// }
|
||||
|
||||
// // =========================
|
||||
// // DTOs
|
||||
// // =========================
|
||||
// public sealed class AiSelectRequest
|
||||
// {
|
||||
// public string Query { get; set; } = "";
|
||||
// public List<string> Allowed { get; set; } = new();
|
||||
// public bool Debug { get; set; } = false;
|
||||
// }
|
||||
|
||||
// public sealed class AiSelectResponse
|
||||
// {
|
||||
// [JsonPropertyName("names")]
|
||||
// public List<string> Names { get; set; } = new();
|
||||
|
||||
// [JsonPropertyName("debug")]
|
||||
// public object? Debug { get; set; }
|
||||
// }
|
||||
|
||||
// // =========================
|
||||
// // POST /api/ai/select
|
||||
// // =========================
|
||||
// [HttpPost("select")]
|
||||
// public async Task<IActionResult> Select([FromBody] AiSelectRequest body)
|
||||
// {
|
||||
// if (body is null) return BadRequest(new { error = "Body requis." });
|
||||
// if (string.IsNullOrWhiteSpace(body.Query)) return BadRequest(new { error = "Query requis." });
|
||||
// if (body.Allowed is null || body.Allowed.Count == 0)
|
||||
// {
|
||||
// _log.LogWarning("AI Select: Allowed vide pour query '{Query}'", body.Query);
|
||||
// return Ok(new AiSelectResponse { Names = new() });
|
||||
// }
|
||||
|
||||
// // Normalisation unique
|
||||
// var allowed = body.Allowed
|
||||
// .Where(a => !string.IsNullOrWhiteSpace(a))
|
||||
// .Select(a => a.Trim())
|
||||
// .Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
// .ToList();
|
||||
|
||||
// // --- 1) Raccourcis instantanés : catégories & synonymes riches
|
||||
// var catHits = TryCategoryExpand(body.Query, allowed);
|
||||
// if (catHits.Count > 0)
|
||||
// return Ok(new AiSelectResponse { Names = SortByAllowedOrder(catHits, allowed) });
|
||||
|
||||
// // --- 2) Match "classique" tolérant (FR/EN, accents, pluriels)
|
||||
// var classicHits = ClassicMatch(body.Query, allowed);
|
||||
// if (classicHits.Count > 0)
|
||||
// return Ok(new AiSelectResponse { Names = SortByAllowedOrder(classicHits, allowed) });
|
||||
|
||||
// // --- 3) Heuristique "prompt" -> on tente l'IA (court)
|
||||
// var ollamaUrl = _cfg["Ollama:Url"] ?? "http://ollama:11434";
|
||||
// var model = _cfg["Ollama:Model"] ?? "qwen2.5:1.5b-instruct";
|
||||
|
||||
// // prompt ultra directif pour forcer un tableau JSON plat
|
||||
// var prompt =
|
||||
// $"Liste autorisée: [{string.Join(",", allowed.Select(a => $"\"{a}\""))}].\n" +
|
||||
// $"Requête: \"{body.Query}\".\n" +
|
||||
// "Réponds UNIQUEMENT par un tableau JSON plat des éléments EXACTEMENT issus de la liste autorisée. " +
|
||||
// "Exemple: [\"Tomate\",\"Oignon\"]. Interdit: objets {name:...}, clés ingredients:, texte hors JSON.";
|
||||
|
||||
// var client = _httpFactory.CreateClient();
|
||||
// client.Timeout = Timeout.InfiniteTimeSpan; // on contrôle avec CTS
|
||||
|
||||
// var payload = new
|
||||
// {
|
||||
// model,
|
||||
// prompt,
|
||||
// stream = false,
|
||||
// format = "json",
|
||||
// keep_alive = "10m",
|
||||
// options = new
|
||||
// {
|
||||
// temperature = 0.0,
|
||||
// num_ctx = 64,
|
||||
// num_predict = 64
|
||||
// }
|
||||
// };
|
||||
|
||||
// var req = new HttpRequestMessage(HttpMethod.Post, $"{ollamaUrl.TrimEnd('/')}/api/generate");
|
||||
// req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
// req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
|
||||
// _log.LogInformation("AI Select ▶ Query='{Query}', AllowedCount={AllowedCount}, Model={Model}, Url={Url}",
|
||||
// body.Query, allowed.Count, model, ollamaUrl);
|
||||
// if (body.Debug)
|
||||
// {
|
||||
// _log.LogDebug("AI Select ▶ Prompt envoyé:\n{Prompt}", payload.prompt);
|
||||
// }
|
||||
|
||||
// // Si la requête ressemble à une phrase libre -> 4s, sinon 2s
|
||||
// bool prompty = body.Query.Length >= 18
|
||||
// || Regex.IsMatch(body.Query, @"\b(avec|sans|pour|idée|idées|recette|faire|préparer|quoi|envie)\b", RegexOptions.IgnoreCase);
|
||||
// using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(prompty ? 4 : 2));
|
||||
|
||||
// try
|
||||
// {
|
||||
// using var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
// var raw = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
// _log.LogInformation("AI Select ◀ HTTP {Status} ({Reason})", (int)resp.StatusCode, resp.ReasonPhrase);
|
||||
// _log.LogDebug("AI Select ◀ RAW:\n{Raw}", raw);
|
||||
|
||||
// if (!resp.IsSuccessStatusCode)
|
||||
// {
|
||||
// _log.LogWarning("AI Select: statut non OK, on renvoie l’erreur au client.");
|
||||
// return StatusCode((int)resp.StatusCode, new
|
||||
// {
|
||||
// error = "IA error",
|
||||
// raw,
|
||||
// url = $"{ollamaUrl.TrimEnd('/')}/api/generate"
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Extraire "response" puis convertir en liste de chaînes
|
||||
// using var doc = JsonDocument.Parse(raw);
|
||||
// var responseText = doc.RootElement.TryGetProperty("response", out var rEl)
|
||||
// ? (rEl.GetString() ?? "")
|
||||
// : "";
|
||||
|
||||
// var iaNames = ExtractNames(responseText);
|
||||
// var mapped = MapToAllowed(iaNames, allowed);
|
||||
|
||||
// return Ok(new AiSelectResponse
|
||||
// {
|
||||
// Names = SortByAllowedOrder(mapped, allowed),
|
||||
// Debug = body.Debug ? new { sent = payload, url = $"{ollamaUrl.TrimEnd('/')}/api/generate", raw } : null
|
||||
// });
|
||||
// }
|
||||
// catch (TaskCanceledException)
|
||||
// {
|
||||
// _log.LogWarning("AI Select: timeout côté Ollama ({Url})", ollamaUrl);
|
||||
// return StatusCode(504, new { error = "Timeout IA" });
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _log.LogError(ex, "AI Select: exception pendant l’appel Ollama.");
|
||||
// return StatusCode(500, new { error = ex.Message });
|
||||
// }
|
||||
// }
|
||||
|
||||
// // =========================
|
||||
// // Helpers — Normalisation
|
||||
// // =========================
|
||||
// static string RemoveDiacritics(string s)
|
||||
// {
|
||||
// if (string.IsNullOrEmpty(s)) return "";
|
||||
// var formD = s.Normalize(NormalizationForm.FormD);
|
||||
// var sb = new StringBuilder(formD.Length);
|
||||
// foreach (var ch in formD)
|
||||
// {
|
||||
// var uc = System.Globalization.CharUnicodeInfo.GetUnicodeCategory(ch);
|
||||
// if (uc != System.Globalization.UnicodeCategory.NonSpacingMark)
|
||||
// sb.Append(ch);
|
||||
// }
|
||||
// return sb.ToString().Normalize(NormalizationForm.FormC);
|
||||
// }
|
||||
|
||||
// static string Key(string? s)
|
||||
// {
|
||||
// s ??= "";
|
||||
// var noAcc = RemoveDiacritics(s).ToLowerInvariant();
|
||||
// // retire ponctuation + normalise espaces
|
||||
// noAcc = Regex.Replace(noAcc, @"[^\p{L}\p{Nd}\s]", " ");
|
||||
// noAcc = Regex.Replace(noAcc, @"\s+", " ").Trim();
|
||||
// // singulier approximatif (enlève 's' final si pertinent)
|
||||
// if (noAcc.EndsWith("s") && noAcc.Length > 3) noAcc = noAcc[..^1];
|
||||
// return noAcc;
|
||||
// }
|
||||
|
||||
// static List<string> SortByAllowedOrder(IEnumerable<string> items, List<string> allowed)
|
||||
// {
|
||||
// var set = new HashSet<string>(items, StringComparer.OrdinalIgnoreCase);
|
||||
// var ordered = new List<string>();
|
||||
// foreach (var a in allowed)
|
||||
// if (set.Contains(a)) ordered.Add(a);
|
||||
// return ordered;
|
||||
// }
|
||||
|
||||
// // =========================
|
||||
// // Helpers — Matching local
|
||||
// // =========================
|
||||
|
||||
// // Synonymes simples EN -> FR (complète librement)
|
||||
// static readonly Dictionary<string, string[]> EN_FR_SYNONYMS = new(StringComparer.OrdinalIgnoreCase)
|
||||
// {
|
||||
// ["meat"] = new[] { "viande", "viandes", "boeuf", "bœuf", "porc", "poulet", "agneau", "veau", "saucisse", "jambon" },
|
||||
// ["fish"] = new[] { "poisson", "saumon", "thon", "cabillaud", "lieu", "filet de lieu", "colin", "merlu" },
|
||||
// ["spicy"] = new[] { "piquant", "épicé", "piment", "harissa", "sriracha", "curry", "paprika", "poivre", "gingembre" },
|
||||
// ["fresh"] = new[] { "frais", "fraîche", "fraîches", "réfrigéré", "perissable", "périssable" },
|
||||
// ["dairy"] = new[] { "laitier", "laitiers", "lait", "beurre", "fromage", "crème", "yaourt" },
|
||||
// ["herbs"] = new[] { "herbes", "persil", "ciboulette", "basilic", "menthe", "aneth", "romarin", "thym" },
|
||||
// ["sauce"] = new[] { "sauce", "sauces", "condiment", "condiments", "mayonnaise", "ketchup", "moutarde", "soja", "sauce soja", "BBQ" },
|
||||
// ["veggie"] = new[] { "légume", "légumes", "tomate", "oignon", "carotte", "brocoli", "courgette", "poivron", "ail", "salade" },
|
||||
// ["protein"] = new[] { "protéine", "protéines", "oeuf", "œuf", "tofu", "tempeh", "pois chiches", "lentilles", "haricots", "poulet", "fromage" },
|
||||
// };
|
||||
|
||||
// static List<string> ClassicMatch(string query, List<string> allowed)
|
||||
// {
|
||||
// var nq = Key(query);
|
||||
// var tokens = nq.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
// // EN->FR élargit
|
||||
// var expanded = new HashSet<string>(tokens);
|
||||
// foreach (var t in tokens)
|
||||
// if (EN_FR_SYNONYMS.TryGetValue(t, out var frs))
|
||||
// foreach (var fr in frs) expanded.Add(Key(fr));
|
||||
|
||||
// var hits = new List<string>();
|
||||
// foreach (var a in allowed)
|
||||
// {
|
||||
// if (AnyTokenMatches(a, expanded))
|
||||
// hits.Add(a);
|
||||
// }
|
||||
|
||||
// return hits.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
// }
|
||||
|
||||
|
||||
// // =========================
|
||||
// // Catégories & règles
|
||||
// // =========================
|
||||
// sealed class CategoryRule
|
||||
// {
|
||||
// public string Name { get; init; } = "";
|
||||
// public string[] Triggers { get; init; } = Array.Empty<string>();
|
||||
// public string[] Must { get; init; } = Array.Empty<string>();
|
||||
// public string[] Any { get; init; } = Array.Empty<string>();
|
||||
// public string[] Not { get; init; } = Array.Empty<string>();
|
||||
// }
|
||||
|
||||
// static readonly List<CategoryRule> CATEGORY_RULES = new()
|
||||
// {
|
||||
// new CategoryRule {
|
||||
// Name = "legumes",
|
||||
// Triggers = new[]{"legume","légume","légumes","vegetable","veggie","crudité"},
|
||||
// Any = new[]{
|
||||
// "oignon","carotte","pomme de terre","brocoli","courgette",
|
||||
// "poivron","tomate","ail","salade","champignon","haricot vert","épinard"
|
||||
// },
|
||||
// Not = new[]{
|
||||
// // viande/charcuterie
|
||||
// "saucisse","porc","poulet","boeuf","bœuf","agneau","veau","jambon","lardon","charcuterie","filet",
|
||||
// // poisson/mer
|
||||
// "poisson","saumon","thon","lieu","cabillaud","merlu","colin","crevette","moule",
|
||||
// // laitier/fromage/œuf
|
||||
// "fromage","crème","lait","yaourt","œuf","oeuf","beurre"
|
||||
// }
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "fruits",
|
||||
// Triggers = new[]{"fruit","fruits","dessert","sucré"},
|
||||
// Any = new[]{"pomme","banane","orange","citron","citron vert","fraise","poire","raisin","tomate"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "epices",
|
||||
// Triggers = new[]{"epice","épice","epices","épices","spice","spicy","assaisonnement"},
|
||||
// Any = new[]{"cumin","paprika","curcuma","vadouvan","curry","poivre","piment","gingembre","muscade","thym"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "herbes",
|
||||
// Triggers = new[]{"herbe","herbes","aromatique","aromatiques"},
|
||||
// Any = new[]{"persil","ciboulette","basilic","menthe","aneth","romarin","thym","coriandre"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "laitiers",
|
||||
// Triggers = new[]{"laitier","laitiers","dairy","fromager","crémier"},
|
||||
// Any = new[]{"lait","beurre","fromage","crème","yaourt","crème aigre","mozzarella","parmesan","lait de coco"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "viandes",
|
||||
// Triggers = new[]{"viande","viandes","carné","meat","steak","charcuterie"},
|
||||
// Any = new[]{"boeuf","bœuf","porc","poulet","dinde","agneau","veau","saucisse","jambon","lardons","filet de porc"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "poissons",
|
||||
// Triggers = new[]{"poisson","poissons","seafood","poissonnerie","poissonnier"},
|
||||
// Any = new[]{"saumon","thon","lieu","filet de lieu","cabillaud","merlu","colin","crevette","moule"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "proteines",
|
||||
// Triggers = new[]{"protéine","protéines","protein","proteins"},
|
||||
// Any = new[]{"oeuf","œuf","tofu","tempeh","pois chiches","lentilles","haricots","saucisse","jambon","poulet","filet de porc","fromage"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "frais",
|
||||
// Triggers = new[]{"frais","fraiche","fraîche","fraîches","réfrigéré","frigo","perissable","périssable"},
|
||||
// Any = new[]{"salade","tomate","concombre","yaourt","fromage","beurre","crème","herbes","viande","poisson"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "piquant",
|
||||
// Triggers = new[]{"piquant","pimenté","fort","épicé","spicy","hot"},
|
||||
// Any = new[]{"piment","harissa","sriracha","tabasco","curry","paprika","gingembre","poivre","pâte de curry","pâte de curry vert"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "sauces",
|
||||
// Triggers = new[]{"sauce","sauces","condiment","condiments","topping"},
|
||||
// Any = new[]{"mayonnaise","ketchup","moutarde","soja","sauce soja","worcestershire","BBQ","harissa","sriracha","pâte de curry","pâte de curry vert","lait de coco","crème"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "condiments",
|
||||
// Triggers = new[]{"condiment","condiments","pickles","vinaigre","cornichon"},
|
||||
// Any = new[]{"moutarde","vinaigre","huile","cornichon","câpres","poivre","sel"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "cereales",
|
||||
// Triggers = new[]{"céréale","céréales","grains","cereal"},
|
||||
// Any = new[]{"riz","quinoa","semoule","boulgour","avoine","farine"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "pates",
|
||||
// Triggers = new[]{"pate","pâtes","nouilles","pasta","spaghetti"},
|
||||
// Any = new[]{"pâtes","nouilles","nouilles de riz","spaghetti","penne","farfalle"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "sandwich",
|
||||
// Triggers = new[]{"sandwich","sandwichs","sandwiches","wrap","tacos","kebab","burger"},
|
||||
// Any = new[]{"pain","salade","tomate","oignon","fromage","jambon","saucisse","poulet","sauce","cornichon"}
|
||||
// },
|
||||
// new CategoryRule {
|
||||
// Name = "sucre",
|
||||
// Triggers = new[]{"sucre","sucré","dessert","patisserie","pâtisserie","gâteau"},
|
||||
// Any = new[]{"sucre","chocolat","vanille","beurre","farine","lait","œuf","oeuf","pomme","banane","crème"}
|
||||
// },
|
||||
// };
|
||||
|
||||
// static List<string> TryCategoryExpand(string query, List<string> allowed)
|
||||
// {
|
||||
// var nq = Key(query);
|
||||
// var hits = new List<string>();
|
||||
|
||||
// foreach (var rule in CATEGORY_RULES)
|
||||
// {
|
||||
// if (!rule.Triggers.Any(t => nq.Contains(Key(t)))) continue;
|
||||
|
||||
// var allowedHits = allowed.Where(a =>
|
||||
// {
|
||||
// // normalisé dans les helpers
|
||||
// bool anyOk = rule.Any.Length == 0 || AnyTokenMatches(a, rule.Any);
|
||||
// bool mustOk = rule.Must.Length == 0 || AllTokensMatch(a, rule.Must);
|
||||
// bool notOk = rule.Not.Length > 0 && AnyTokenMatches(a, rule.Not);
|
||||
// return anyOk && mustOk && !notOk;
|
||||
// });
|
||||
|
||||
// hits.AddRange(allowedHits);
|
||||
// }
|
||||
|
||||
// return hits.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
// }
|
||||
|
||||
// // =========================
|
||||
// // Helpers — Parsing IA
|
||||
// // =========================
|
||||
// static List<string> ExtractNames(string responseText)
|
||||
// {
|
||||
// // 1) tableau JSON brut -> ["...","..."]
|
||||
// try
|
||||
// {
|
||||
// var arr = JsonSerializer.Deserialize<string[]>(responseText);
|
||||
// if (arr is not null && arr.Length > 0)
|
||||
// return arr.Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
|
||||
// }
|
||||
// catch { /* not an array */ }
|
||||
|
||||
// // 2) objet { "ingredients": [...] } ou { "ingrédients": [...] }
|
||||
// try
|
||||
// {
|
||||
// using var inner = JsonDocument.Parse(responseText);
|
||||
// if (inner.RootElement.TryGetProperty("ingredients", out var arr1) && arr1.ValueKind == JsonValueKind.Array)
|
||||
// return arr1.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
|
||||
|
||||
// if (inner.RootElement.TryGetProperty("ingrédients", out var arr2) && arr2.ValueKind == JsonValueKind.Array)
|
||||
// return arr2.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
|
||||
// }
|
||||
// catch { /* not an object */ }
|
||||
|
||||
// // 3) JSON encodé dans une string (rare mais vu chez Qwen)
|
||||
// var mObj = Regex.Match(responseText, @"\{[\s\S]*\}");
|
||||
// if (mObj.Success)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// using var objDoc = JsonDocument.Parse(mObj.Value);
|
||||
// if (objDoc.RootElement.TryGetProperty("ingredients", out var arr1) && arr1.ValueKind == JsonValueKind.Array)
|
||||
// return arr1.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
|
||||
|
||||
// if (objDoc.RootElement.TryGetProperty("ingrédients", out var arr2) && arr2.ValueKind == JsonValueKind.Array)
|
||||
// return arr2.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
|
||||
// }
|
||||
// catch { /* ignore */ }
|
||||
// }
|
||||
|
||||
// // 4) fallback : extrait le premier [...] plausible
|
||||
// var mArr = Regex.Match(responseText, @"\[[\s\S]*?\]");
|
||||
// if (mArr.Success)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// var arr = JsonSerializer.Deserialize<string[]>(mArr.Value);
|
||||
// if (arr is not null && arr.Length > 0)
|
||||
// return arr.Where(s => !string.IsNullOrWhiteSpace(s)).ToList();
|
||||
// }
|
||||
// catch { /* ignore */ }
|
||||
// }
|
||||
|
||||
// return new List<string>();
|
||||
// }
|
||||
|
||||
// static List<string> MapToAllowed(IEnumerable<string> names, List<string> allowed)
|
||||
// {
|
||||
// var mapAllowed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
// foreach (var a in allowed)
|
||||
// {
|
||||
// var k = Key(a);
|
||||
// if (!mapAllowed.ContainsKey(k))
|
||||
// mapAllowed[k] = a;
|
||||
// }
|
||||
|
||||
// var result = new List<string>();
|
||||
// var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// foreach (var n in names)
|
||||
// {
|
||||
// var k = Key(n);
|
||||
// if (mapAllowed.TryGetValue(k, out var official))
|
||||
// {
|
||||
// if (seen.Add(official))
|
||||
// result.Add(official);
|
||||
// }
|
||||
// }
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// // --- Helpers de matching précis ---
|
||||
// static bool ContainsPhraseOrWord(string haystack, string token)
|
||||
// {
|
||||
// // les deux sont déjà normalisés via Key() avant appel
|
||||
// if (string.IsNullOrWhiteSpace(token)) return false;
|
||||
|
||||
// if (token.Contains(' '))
|
||||
// {
|
||||
// // multi-mots : on garde le contains sur la phrase normalisée
|
||||
// return haystack.Contains(token);
|
||||
// }
|
||||
|
||||
// // 1 mot : on exige un match sur "mot entier"
|
||||
// // ex: "oignon" ne doit pas faire matcher "saucisse de porc ... aux oignons caramélisés"
|
||||
// var words = haystack.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
// foreach (var w in words)
|
||||
// if (w.Equals(token, StringComparison.Ordinal)) return true;
|
||||
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// static bool AnyTokenMatches(string haystack, IEnumerable<string> tokens)
|
||||
// {
|
||||
// var h = Key(haystack);
|
||||
// foreach (var t in tokens)
|
||||
// {
|
||||
// var tok = Key(t);
|
||||
// if (ContainsPhraseOrWord(h, tok)) return true;
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// static bool AllTokensMatch(string haystack, IEnumerable<string> tokens)
|
||||
// {
|
||||
// var h = Key(haystack);
|
||||
// foreach (var t in tokens)
|
||||
// {
|
||||
// var tok = Key(t);
|
||||
// if (!ContainsPhraseOrWord(h, tok)) return false;
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// }
|
||||
//}
|
||||
Reference in New Issue
Block a user