MoralesLarios.Blazor.Ia.IaFilterColection 1.0.0

dotnet add package MoralesLarios.Blazor.Ia.IaFilterColection --version 1.0.0
                    
NuGet\Install-Package MoralesLarios.Blazor.Ia.IaFilterColection -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MoralesLarios.Blazor.Ia.IaFilterColection" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MoralesLarios.Blazor.Ia.IaFilterColection" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="MoralesLarios.Blazor.Ia.IaFilterColection" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MoralesLarios.Blazor.Ia.IaFilterColection --version 1.0.0
                    
#r "nuget: MoralesLarios.Blazor.Ia.IaFilterColection, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MoralesLarios.Blazor.Ia.IaFilterColection@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MoralesLarios.Blazor.Ia.IaFilterColection&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=MoralesLarios.Blazor.Ia.IaFilterColection&version=1.0.0
                    
Install as a Cake Tool

MoralesLarios.Blazor.Ia.IaFilterColection

Librería de componentes Blazor para filtrar colecciones en lenguaje natural usando IA.

El usuario escribe (o dicta) algo como «delanteros del Liverpool con valoración superior a 85 y menos de 28 años», un proveedor de IA lo traduce a un AST JSON, y la librería lo compila a una Expression<Func<T, bool>> que se aplica sobre cualquier IEnumerable<T> en memoria.

Sin dependencias de librerías comerciales de UI. El paquete trae sus propios iconos SVG, su propia ventana modal y su propio CSS aislado. Funciona con cualquier grid, tabla o foreach.


Índice

  1. Características
  2. Cómo funciona
  3. Instalación
  4. Configuración
  5. Uso básico
  6. API de AiFilterPanel<TItem>
  7. Catálogo de campos y alias
  8. Proveedores de IA soportados
  9. El AST JSON
  10. Uso del pipeline sin interfaz
  11. Reconocimiento de voz
  12. Personalización visual
  13. Superficie pública
  14. Ejemplo completo

Características

🧠 Lenguaje natural El prompt del usuario se convierte en un filtro LINQ real.
🎙️ Dictado por voz Reconocimiento de voz del navegador integrado (BCP-47 configurable).
🔌 Multiproveedor Ollama (local), Groq, Google Gemini y Azure OpenAI.
🧩 Genérico AiFilterPanel<TItem> funciona con cualquier tipo: record, class, POCO...
👀 Vista previa Muestra cuántos elementos sobrevivirían al filtro antes de aplicarlo.
🧬 Editor de AST Ventana emergente para revisar y reejecutar el JSON generado por la IA.
📉 Contraído por defecto Solo ocupa una barra hasta que el usuario lo despliega.
🎨 CSS aislado No contamina ni hereda estilos de tu aplicación.
🚫 Cero ramas por proveedor Ningún if por modelo o proveedor: comportamiento uniforme.

Cómo funciona

flowchart LR
    A["Prompt del usuario<br/>texto o voz"] --> B["MlFilterPromptBuilder&lt;T&gt;<br/>construye el prompt<br/>con el catálogo de campos"]
    B --> C["Proveedor de IA<br/>Ollama · Groq · Gemini · Azure"]
    C --> D["AST JSON"]
    D --> E["FilterPipeline.Apply&lt;T&gt;<br/>normaliza alias y valida campos"]
    E --> F["AstFilterCompiler<br/>Expression&lt;Func&lt;T,bool&gt;&gt;"]
    F --> G["Vista previa<br/>+ chips explicativos"]
    G --> H["OnFilterApplied<br/>IReadOnlyList&lt;TItem&gt;"]

El componente nunca modifica tu colección original: te devuelve la lista filtrada por OnFilterApplied y tú decides qué hacer con ella.


Instalación

dotnet add package MoralesLarios.Blazor.Ia.IaFilterColection

El paquete arrastra automáticamente MoralesLarios.Ai (el motor de IA).

Requisitos

  • .NET 10.0 o superior
  • Blazor Server o Blazor WebAssembly (con render mode interactivo)

Configuración

1. Registrar los servicios en Program.cs

La forma más cómoda es leer las opciones de appsettings.json:

using MoralesLarios.Ai.Config;
using MoralesLarios.Blazor.Ia.IaFilterColection;

var builder = WebApplication.CreateBuilder(args);

// Motor de IA + servicios de la librería (voz) en una sola llamada.
builder.Services.AddIaFilterColection(options =>
{
    builder.Configuration.GetSection(MlAiOptions.SectionName).Bind(options);
});

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

También puedes configurarlo por código, sin appsettings.json:

builder.Services.AddIaFilterColection(options =>
{
    options.Provider   = "Groq";
    options.MaxRetries = 4;
    options.Groq.Endpoint = "https://api.groq.com/openai/v1";
    options.Groq.Model    = "openai/gpt-oss-120b";
    options.Groq.ApiKey   = builder.Configuration["Groq:ApiKey"]!;
});

Si tu aplicación ya llama a AddAi(...) por su cuenta, registra solo los servicios de la librería de componentes:

builder.Services.AddIaFilterColectionComponents();

2. Añadir el script de reconocimiento de voz

En App.razor (o index.html en WebAssembly), antes del cierre de </body>:

<script src="_content/MoralesLarios.Blazor.Ia.IaFilterColection/js/speech-recognition.js"></script>

Si omites este script el panel sigue funcionando: solo se desactiva el botón de micrófono.

3. Declarar los @using en _Imports.razor

@using MoralesLarios.Blazor.Ia.IaFilterColection.Components
@using MoralesLarios.Blazor.Ia.IaFilterColection.DynamicFilters
@using MoralesLarios.Blazor.Ia.IaFilterColection.Services

4. appsettings.json

{
  "Ai": {
    "Provider": "Groq",      // Groq | Ollama | Gemini | AzureOpenAI
    "MaxRetries": 4,

    "Groq": {
      "Endpoint": "https://api.groq.com/openai/v1",
      "Model": "openai/gpt-oss-120b",
      "ApiKey": "gsk_..."
    },
    "Ollama": {
      "Endpoint": "http://localhost:11434",
      "Model": "qwen2.5:32b"
    },
    "Gemini": {
      "Endpoint": "https://generativelanguage.googleapis.com/v1beta/openai/",
      "Model": "gemini-2.0-flash-lite",
      "ApiKey": "..."
    },
    "AzureOpenAI": {
      "Endpoint": "https://mi-recurso.openai.azure.com/",
      "ApiKey": "...",
      "Deployment": "gpt-4o-mini"
    }
  }
}

⚠️ No publiques claves en el repositorio. Usa user secrets, variables de entorno o Azure Key Vault.


Uso básico

@page "/jugadores"
@rendermode InteractiveServer

<AiFilterPanel TItem="PlayerRecord"
               Source="@_allPlayers"
               OnFilterApplied="HandleFilterApplied"
               OnFilterReset="HandleFilterReset" />

<ul>
    @foreach (var p in _visiblePlayers)
    {
        <li>@p.Name — @p.Team (@p.Overall)</li>
    }
</ul>

@code {
    private IReadOnlyList<PlayerRecord> _allPlayers     = [];
    private IReadOnlyList<PlayerRecord> _visiblePlayers = [];

    private void HandleFilterApplied(IReadOnlyList<PlayerRecord> filtered) => _visiblePlayers = filtered;
    private void HandleFilterReset() => _visiblePlayers = _allPlayers;
}

Es importante mantener dos colecciones: Source debe recibir siempre el conjunto completo (si le pasas la ya filtrada, los filtros se irían acumulando).


API de AiFilterPanel<TItem>

Datos

Parámetro Tipo Por defecto Descripción
Source IReadOnlyList<TItem> [] Requerido. Conjunto completo sobre el que se evalúa el filtro.
OnFilterApplied EventCallback<IReadOnlyList<TItem>> — Se dispara al aplicar el filtro. Recibe los elementos que lo cumplen.
OnFilterReset EventCallback — Se dispara al resetear. El consumidor debe volver a mostrar todo.

Estado contraído

Parámetro Tipo Por defecto Descripción
Collapsed bool true Si es true solo se muestra el lanzador compacto.
CollapsedChanged EventCallback<bool> — Permite @bind-Collapsed.
CollapsedTitle string "Filtrar con IA" Título del lanzador.
CollapsedHint string "Describe el filtro en lenguaje natural" Texto secundario del lanzador.

Presentación

Parámetro Tipo Por defecto Descripción
Title string "Filter Studio · Laboratorio de pruebas" Insignia superior.
Hint string "Describe en lenguaje natural…" Pista bajo la cabecera.
Placeholder string (ejemplo largo) Texto de ejemplo del textarea.
GenerateButtonText string "Generar filtro con IA" Rótulo del botón principal.
AriaLabel string "Laboratorio de filtros con IA" Etiqueta accesible de la sección.
ResultTitle string "Respuesta del pipeline" Título de la columna de resultados.
ResultSubtitle string "Vista previa antes de aplicar" Subtítulo de la columna de resultados.
SpeechLanguage string "es-ES" Idioma BCP-47 del reconocimiento de voz.
ShowAstEditor bool true false oculta por completo el editor de AST.

Ejemplo con todo personalizado

<AiFilterPanel TItem="OrderRecord"
               Source="@_allOrders"
               OnFilterApplied="OnApplied"
               OnFilterReset="OnReset"
               @bind-Collapsed="_panelCollapsed"
               CollapsedTitle="Buscar pedidos"
               CollapsedHint="Pregunta en lenguaje natural"
               Title="Asistente de pedidos"
               Hint="Ejemplo: pedidos de Madrid de más de 500 € del último trimestre"
               Placeholder="Escribe o dicta tu consulta..."
               GenerateButtonText="Buscar"
               ResultTitle="Coincidencias"
               ResultSubtitle="Revisa antes de aplicar"
               SpeechLanguage="es-ES"
               ShowAstEditor="false" />

Catálogo de campos y alias

El catálogo de campos determina qué propiedades puede usar la IA. Por defecto se construye por reflexión (hasta 3 niveles de anidamiento) sin necesidad de configurar nada.

Registrar un catálogo explícito con alias de dominio mejora mucho la precisión: permite que la IA use vocabulario humano y el pipeline lo traduzca a la ruta canónica.

using MoralesLarios.Blazor.Ia.IaFilterColection.DynamicFilters;

public static class PlayerFieldCatalog
{
    public const int MaxDepth = 3;

    public static IReadOnlyDictionary<string, string> BuildAliases() =>
        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["nombre"]     = "Name",
            ["jugador"]    = "Name",
            ["equipo"]     = "Team",
            ["club"]       = "Team",
            ["valoracion"] = "Overall",
            ["ovr"]        = "Overall",
            ["edad"]       = "Age",
            ["pais"]       = "Nationality",
            ["posicion"]   = "Position",
            ["pierna"]     = "PreferredFoot",
        };
}

Y en Program.cs, antes de construir la aplicación:

FieldCatalogs.Register<PlayerRecord>(PlayerFieldCatalog.BuildAliases(), PlayerFieldCatalog.MaxDepth);

API de catálogos

Miembro Descripción
FieldCatalogs.Register<T>(aliases, maxDepth = 3) Registra el catálogo de T con sus alias.
FieldCatalogs.For<T>() Obtiene el catálogo de T (lo crea por reflexión si no existe).
IFieldCatalog.AllowedPaths Rutas válidas ("Release.Console", "Team"...).
IFieldCatalog.Normalize(raw) Traduce alias → ruta canónica.
IFieldCatalog.IsValidPath(name) Indica si la ruta existe en el tipo.
IFieldCatalog.TryGetCanonical(name, out canonical) Resuelve la ruta canónica exacta.

Si la IA inventa un campo inexistente, el pipeline no lanza excepción: devuelve errores legibles que el panel muestra al usuario.


Proveedores de IA soportados

Proveedor Valor de Provider Notas
Ollama Ollama Local y gratuito. Requiere Endpoint y Model.
Groq Groq Nube muy rápida, endpoint compatible con OpenAI. Requiere ApiKey.
Google Gemini Gemini Endpoint compatible con OpenAI. Requiere ApiKey.
Azure OpenAI AzureOpenAI Requiere Endpoint, ApiKey y Deployment.

El panel muestra un selector nativo con todos los proveedores que tengan configuración válida, de modo que el usuario puede cambiar de motor en caliente sin reiniciar la aplicación. El proveedor de appsettings.json es el preseleccionado.

MaxRetries controla los reintentos ante errores transitorios (429, 503, timeouts).


El AST JSON

El contrato entre la IA y el compilador es un árbol de sintaxis con raíz expr:

{
  "expr": {
    "kind": "binary",
    "op": "and",
    "left":  { "kind": "binary", "op": "==", "left": { "kind": "field", "name": "Team" },    "right": { "kind": "const", "value": "Liverpool" } },
    "right": { "kind": "binary", "op": ">",  "left": { "kind": "field", "name": "Overall" }, "right": { "kind": "const", "value": 85 } }
  }
}

Tipos de nodo

kind Record Campos
const ConstNode value
field FieldNode name (también acepta path)
binary BinaryNode op, left, right
unary UnaryNode op, operand
func FuncNode name, args, caseSensitive?

Operadores y funciones

  • Lógicos: and, or, not
  • Comparación: ==, !=, >, >=, <, <=
  • Aritméticos: +, -, *, /, %
  • Cadenas: contains (alias like, matches), startswith, endswith, lower, upper, length
  • Fechas: year, month, day
  • Números: abs, round
  • Conjuntos: in

El deserializador es tolerante: acepta path en lugar de name, starts_with en lugar de startswith, contains como operador binario, y no exige que kind esté siempre presente. Esto absorbe las variaciones entre modelos sin ninguna rama condicional por proveedor.

El editor de AST del panel (icono de código en la cabecera) abre una ventana modal donde el usuario puede corregir el JSON a mano y reejecutarlo.


Uso del pipeline sin interfaz

Todo el motor es utilizable sin el componente visual, por ejemplo desde una API:

using MoralesLarios.Blazor.Ia.IaFilterColection.DynamicFilters;

FilterResult<PlayerRecord> result = FilterPipeline.Apply(players, astJson);

if (!result.Success)
{
    foreach (var error in result.Errors)
    {
        logger.LogWarning("Filtro inválido: {Error}", error);
    }
    return;
}

List<PlayerRecord> filtered = result.Items;
string canonicalJson = result.NormalizedJson;   // AST con alias ya resueltos

Compilar el predicado directamente (útil para EF Core en memoria o para reutilizarlo):

var root = JsonSerializer.Deserialize<FilterRoot>(astJson, FilterPreviewBuilder.AstJsonOptions)!;
Expression<Func<PlayerRecord, bool>> predicate = AstFilterCompiler.CompilePredicate<PlayerRecord>(root);

Generar los pasos legibles que el panel pinta como chips:

List<FilterPreviewStep> steps = FilterPreviewBuilder.BuildSafe(astJson);
// step.Kind == "rule" -> condición ; step.Kind == "join" -> AND / OR

Reconocimiento de voz

El servicio ISpeechRecognitionService envuelve la Web Speech API del navegador y se registra automáticamente con AddIaFilterColectionComponents(). Puedes inyectarlo por tu cuenta:

@inject ISpeechRecognitionService Speech

Requisitos: navegador compatible (Chrome, Edge), HTTPS (o localhost) y permiso de micrófono. Si el navegador no lo soporta, el botón de micrófono queda deshabilitado sin romper nada.


Personalización visual

El componente usa CSS isolation, por lo que sus estilos no se filtran ni son sobrescritos por accidente. Todas las clases usan el prefijo aifp:

Clase Zona
.aifp Raíz del panel
.aifp--expanded Raíz cuando está desplegado
.aifp__launcher Lanzador compacto (estado contraído)
.aifp__model Selector de proveedor
.aifp-json-overlay / .aifp-json-window Ventana modal del AST

Para ajustar el aspecto, sobrescribe desde un CSS global usando ::deep o envolviendo el panel:

.mi-zona ::deep .aifp__launcher {
    border-radius: 8px;
}

Los estilos llegan al navegador vía el bundle del RCL: _content/MoralesLarios.Blazor.Ia.IaFilterColection/MoralesLarios.Blazor.Ia.IaFilterColection.<hash>.bundle.scp.css, importado automáticamente por el <app>.styles.css de tu aplicación.


Superficie pública

MoralesLarios.Blazor.Ia.IaFilterColection

  • IaFilterColectionServiceCollectionExtensions
    • AddIaFilterColection(Action<MlAiOptions>)
    • AddIaFilterColection(MlAiOptions)
    • AddIaFilterColectionComponents()

…IaFilterColection.Components

  • AiFilterPanel<TItem> — el panel de filtrado
  • MlSvgIcon — icono SVG interno reutilizable
  • MlIcon — enumeración de iconos disponibles

…IaFilterColection.DynamicFilters

  • FilterPipeline — Apply<T>(source, astJson)
  • AstFilterCompiler — CompilePredicate<T>(root)
  • FilterPreviewBuilder — Build, BuildSafe, AstJsonOptions
  • FilterPreviewStep(string Text, string Kind)
  • FilterResult<T> — Items, Errors, Success, NormalizedJson
  • IFieldCatalog, ReflectionFieldCatalog, FieldCatalogs
  • AstNodeConverterTolerant

…IaFilterColection.DynamicFilters.Ast

  • AstNode, ConstNode, FieldNode, BinaryNode, UnaryNode, FuncNode, FilterRoot

…IaFilterColection.Services

  • ISpeechRecognitionService, SpeechRecognitionService

Ejemplo completo

Aplicación Blazor Server de principio a fin: catálogo de jugadores filtrable en lenguaje natural, sin ninguna librería de UI de terceros.

1. MiApp.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="MoralesLarios.Blazor.Ia.IaFilterColection" Version="1.0.0" />
  </ItemGroup>

</Project>

2. El modelo — Data/PlayerRecord.cs

namespace MiApp.Data;

public sealed record PlayerRecord(
    string Name,
    string Team,
    string League,
    string Nationality,
    string Position,
    string PreferredFoot,
    int    Age,
    int    Overall,
    decimal ValueMillions);

3. El catálogo de campos — Data/PlayerFieldCatalog.cs

namespace MiApp.Data;

/// <summary>Alias de dominio para que la IA pueda usar vocabulario humano.</summary>
public static class PlayerFieldCatalog
{
    public const int MaxDepth = 2;

    public static IReadOnlyDictionary<string, string> BuildAliases() =>
        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["nombre"]      = "Name",
            ["jugador"]     = "Name",
            ["equipo"]      = "Team",
            ["club"]        = "Team",
            ["liga"]        = "League",
            ["pais"]        = "Nationality",
            ["nacionalidad"]= "Nationality",
            ["posicion"]    = "Position",
            ["pierna"]      = "PreferredFoot",
            ["edad"]        = "Age",
            ["valoracion"]  = "Overall",
            ["ovr"]         = "Overall",
            ["media"]       = "Overall",
            ["valor"]       = "ValueMillions",
            ["precio"]      = "ValueMillions",
        };
}

4. El servicio de datos — Services/PlayersService.cs

using MiApp.Data;

namespace MiApp.Services;

public interface IPlayersService
{
    IReadOnlyList<PlayerRecord> GetAll();
}

public sealed class PlayersService : IPlayersService
{
    private readonly PlayerRecord[] _players =
    [
        new("Mohamed Salah",     "Liverpool",   "Premier League", "Egipto",    "RW", "Izquierda", 32, 89, 55m),
        new("Virgil van Dijk",   "Liverpool",   "Premier League", "Países Bajos","CB","Derecha",  33, 88, 40m),
        new("Alexis Mac Allister","Liverpool",  "Premier League", "Argentina", "CM", "Derecha",  26, 84, 70m),
        new("Erling Haaland",    "Man City",    "Premier League", "Noruega",   "ST", "Izquierda",24, 91, 180m),
        new("Rodri",             "Man City",    "Premier League", "España",    "CM", "Derecha",  28, 91, 130m),
        new("Vinícius Júnior",   "Real Madrid", "LaLiga",         "Brasil",    "LW", "Derecha",  24, 90, 200m),
        new("Jude Bellingham",   "Real Madrid", "LaLiga",         "Inglaterra","CM", "Derecha",  21, 90, 180m),
        new("Lamine Yamal",      "Barcelona",   "LaLiga",         "España",    "RW", "Izquierda",17, 84, 120m),
    ];

    public IReadOnlyList<PlayerRecord> GetAll() => _players;
}

5. appsettings.json

{
  "Logging": { "LogLevel": { "Default": "Information" } },
  "AllowedHosts": "*",

  "Ai": {
    "Provider": "Groq",
    "MaxRetries": 4,
    "Groq": {
      "Endpoint": "https://api.groq.com/openai/v1",
      "Model": "openai/gpt-oss-120b",
      "ApiKey": ""            // usa dotnet user-secrets para la clave real
    },
    "Ollama": {
      "Endpoint": "http://localhost:11434",
      "Model": "qwen2.5:32b"
    }
  }
}
dotnet user-secrets set "Ai:Groq:ApiKey" "gsk_tu_clave_real"

6. Program.cs

using MiApp.Components;
using MiApp.Data;
using MiApp.Services;

using MoralesLarios.Ai.Config;
using MoralesLarios.Blazor.Ia.IaFilterColection;
using MoralesLarios.Blazor.Ia.IaFilterColection.DynamicFilters;

var builder = WebApplication.CreateBuilder(args);

// 1) Catálogo de campos con alias de dominio (antes de construir la app).
FieldCatalogs.Register<PlayerRecord>(PlayerFieldCatalog.BuildAliases(), PlayerFieldCatalog.MaxDepth);

// 2) Datos de la aplicación.
builder.Services.AddSingleton<IPlayersService, PlayersService>();

// 3) Motor de IA + servicios de la librería (voz).
builder.Services.AddIaFilterColection(options =>
{
    builder.Configuration.GetSection(MlAiOptions.SectionName).Bind(options);
});

// 4) Blazor.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.MapStaticAssets();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

7. Components/App.razor

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="@Assets["app.css"]" />
    <link rel="stylesheet" href="@Assets["MiApp.styles.css"]" />
    <HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
    <Routes @rendermode="InteractiveServer" />

    
    <script src="_content/MoralesLarios.Blazor.Ia.IaFilterColection/js/speech-recognition.js"></script>
    <script src="_framework/blazor.web.js"></script>
</body>
</html>

8. Components/_Imports.razor

@using System.Net.Http
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.JSInterop

@using MiApp
@using MiApp.Components
@using MiApp.Data
@using MiApp.Services

@using MoralesLarios.Blazor.Ia.IaFilterColection.Components
@using MoralesLarios.Blazor.Ia.IaFilterColection.DynamicFilters
@using MoralesLarios.Blazor.Ia.IaFilterColection.Services

9. La página — Components/Pages/Players.razor

@page "/players"
@rendermode InteractiveServer

@inject IPlayersService PlayersService

<PageTitle>Jugadores</PageTitle>

<section class="players">

    <header class="players__head">
        <h1>Catálogo de jugadores</h1>
        <p>
            Mostrando <strong>@VisiblePlayers.Count</strong> de
            <strong>@AllPlayers.Count</strong> jugadores.
            @if (IsFiltered)
            {
                <span class="players__badge">filtrado por IA</span>
            }
        </p>
    </header>

    @* ── Panel de filtrado con IA ──────────────────────────────────────────── *@
    <AiFilterPanel TItem="PlayerRecord"
                   Source="@AllPlayers"
                   OnFilterApplied="HandleFilterApplied"
                   OnFilterReset="HandleFilterReset"
                   CollapsedTitle="Filtrar jugadores con IA"
                   CollapsedHint="Ej.: extremos zurdos del Liverpool con más de 85 de media"
                   Title="Buscador inteligente"
                   Hint="Describe en lenguaje natural qué jugadores quieres ver"
                   Placeholder="Ejemplo: jugadores de LaLiga menores de 25 años con valoración superior a 88..."
                   GenerateButtonText="Generar filtro"
                   SpeechLanguage="es-ES" />

    @* ── Resultados ────────────────────────────────────────────────────────── *@
    @if (VisiblePlayers.Count == 0)
    {
        <p class="players__empty">Ningún jugador cumple el filtro.</p>
    }
    else
    {
        <table class="players__table">
            <thead>
                <tr>
                    <th>Jugador</th>
                    <th>Equipo</th>
                    <th>Liga</th>
                    <th>País</th>
                    <th>Pos.</th>
                    <th>Pierna</th>
                    <th class="num">Edad</th>
                    <th class="num">OVR</th>
                    <th class="num">Valor</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var player in VisiblePlayers)
                {
                    <tr>
                        <td>@player.Name</td>
                        <td>@player.Team</td>
                        <td>@player.League</td>
                        <td>@player.Nationality</td>
                        <td>@player.Position</td>
                        <td>@player.PreferredFoot</td>
                        <td class="num">@player.Age</td>
                        <td class="num">@player.Overall</td>
                        <td class="num">@player.ValueMillions.ToString("N0") M€</td>
                    </tr>
                }
            </tbody>
        </table>
    }

</section>

10. El code-behind — Components/Pages/Players.razor.cs

using Microsoft.AspNetCore.Components;

using MiApp.Data;

namespace MiApp.Components.Pages;

public partial class Players : ComponentBase
{
    /// <summary>Conjunto completo. Es lo que se pasa a <c>Source</c>: nunca se muta.</summary>
    private IReadOnlyList<PlayerRecord> AllPlayers { get; set; } = [];

    /// <summary>Conjunto visible en la tabla.</summary>
    private IReadOnlyList<PlayerRecord> VisiblePlayers { get; set; } = [];

    /// <summary>Indica si hay un filtro de IA activo.</summary>
    private bool IsFiltered => VisiblePlayers.Count != AllPlayers.Count;

    protected override void OnInitialized()
    {
        AllPlayers     = PlayersService.GetAll();
        VisiblePlayers = AllPlayers;
    }

    /// <summary>El panel ya ha evaluado el filtro: solo hay que pintar el resultado.</summary>
    private void HandleFilterApplied(IReadOnlyList<PlayerRecord> filtered) => VisiblePlayers = filtered;

    /// <summary>El usuario ha reseteado: volvemos al conjunto completo.</summary>
    private void HandleFilterReset() => VisiblePlayers = AllPlayers;
}

11. Los estilos — Components/Pages/Players.razor.css

.players {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    padding: 1.5rem;
}

.players__head h1 {
    margin: 0 0 .25rem;
    font-size: 1.6rem;
    font-weight: 700;
}

.players__head p {
    margin: 0;
    color: #64748b;
}

.players__badge {
    display: inline-block;
    margin-left: .5rem;
    padding: .1rem .55rem;
    border-radius: 999px;
    background: #e0f2fe;
    color: #0369a1;
    font-size: .75rem;
    font-weight: 600;
}

.players__empty {
    padding: 2rem;
    border: 1px dashed #cbd5e1;
    border-radius: 12px;
    color: #64748b;
    text-align: center;
}

.players__table {
    width: 100%;
    border-collapse: collapse;
    font-size: .9rem;
    background: #fff;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgb(15 23 42 / .08);
}

.players__table thead th {
    padding: .7rem .9rem;
    background: #f8fafc;
    border-bottom: 1px solid #e2e8f0;
    text-align: left;
    font-weight: 600;
    color: #475569;
}

.players__table tbody td {
    padding: .65rem .9rem;
    border-bottom: 1px solid #f1f5f9;
}

.players__table tbody tr:hover td {
    background: #f8fafc;
}

.players__table .num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

12. Probarlo

dotnet run

Abre https://localhost:7099/players, despliega el panel y escribe:

Prompt AST generado (resumido)
jugadores del Liverpool con valoración mayor de 85 Team == "Liverpool" AND Overall > 85
extremos zurdos menores de 25 años Position contains "W" AND PreferredFoot == "Izquierda" AND Age < 25
jugadores de LaLiga que valgan más de 150 millones League == "LaLiga" AND ValueMillions > 150
españoles o argentinos con más de 84 de media (Nationality == "España" OR Nationality == "Argentina") AND Overall > 84

El panel mostrará el proveedor usado, el tiempo de respuesta, los chips con las reglas interpretadas y los resultados estimados. Al pulsar Aplicar filtro se dispara OnFilterApplied y la tabla se actualiza.


Licencia

MIT © Morales Larios

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 102 9/15/2026