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
<PackageReference Include="MoralesLarios.Blazor.Ia.IaFilterColection" Version="1.0.0" />
<PackageVersion Include="MoralesLarios.Blazor.Ia.IaFilterColection" Version="1.0.0" />
<PackageReference Include="MoralesLarios.Blazor.Ia.IaFilterColection" />
paket add MoralesLarios.Blazor.Ia.IaFilterColection --version 1.0.0
#r "nuget: MoralesLarios.Blazor.Ia.IaFilterColection, 1.0.0"
#:package MoralesLarios.Blazor.Ia.IaFilterColection@1.0.0
#addin nuget:?package=MoralesLarios.Blazor.Ia.IaFilterColection&version=1.0.0
#tool nuget:?package=MoralesLarios.Blazor.Ia.IaFilterColection&version=1.0.0
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
- Características
- Cómo funciona
- Instalación
- Configuración
- Uso básico
- API de
AiFilterPanel<TItem> - Catálogo de campos y alias
- Proveedores de IA soportados
- El AST JSON
- Uso del pipeline sin interfaz
- Reconocimiento de voz
- Personalización visual
- Superficie pública
- 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<T><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<T><br/>normaliza alias y valida campos"]
E --> F["AstFilterCompiler<br/>Expression<Func<T,bool>>"]
F --> G["Vista previa<br/>+ chips explicativos"]
G --> H["OnFilterApplied<br/>IReadOnlyList<TItem>"]
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(aliaslike,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
IaFilterColectionServiceCollectionExtensionsAddIaFilterColection(Action<MlAiOptions>)AddIaFilterColection(MlAiOptions)AddIaFilterColectionComponents()
…IaFilterColection.Components
AiFilterPanel<TItem>— el panel de filtradoMlSvgIcon— icono SVG interno reutilizableMlIcon— enumeración de iconos disponibles
…IaFilterColection.DynamicFilters
FilterPipeline—Apply<T>(source, astJson)AstFilterCompiler—CompilePredicate<T>(root)FilterPreviewBuilder—Build,BuildSafe,AstJsonOptionsFilterPreviewStep(string Text, string Kind)FilterResult<T>—Items,Errors,Success,NormalizedJsonIFieldCatalog,ReflectionFieldCatalog,FieldCatalogsAstNodeConverterTolerant
…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 | Versions 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. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.0)
- MoralesLarios.Ai (>= 1.0.0)
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 |