Découvrez les composants intelligents Telerik et comment les utiliser dans une application Blazor pour tirer parti de l’IA pour une meilleure expérience utilisateur.
Dans un article précédent, je vous ai expliqué ce que Composants intelligents sont et comment vous pouvez les intégrer dans une application Blazor existante. Dans cet article, vous verrez comment des entreprises comme Progress ont poussé le concept à un nouveau niveau en créant des composants intelligents qui permettent aux utilisateurs de vivre des expériences plus puissantes lors de l’utilisation de modèles d’IA. Commençons !
Que sont les composants Telerik Smart (AI) ?
Le progrès Composants Telerik Smart (IA) sont un ensemble de composants expérimentaux qui vous permettent d’ajouter des fonctionnalités d’IA à vos applications .NET. Telerik a créé une série de composants intelligents ciblant différentes plateformes telles que Blazer, Noyau ASP.NET, WPF, WinForms et .FILET.
Il est très important que si vous avez des commentaires sur les composants, vous aidiez l’équipe à façonner son avenir à travers le Composants intelligents Telerik dépôt sur GitHub.
Création et configuration d’un projet pour utiliser les composants Telerik Smart (AI)
La première étape pour transformer les composants Telerik en composants intelligents consiste à configurer correctement le projet pour utiliser les capacités de intégrations et AI models au sein du projet. Pour ce faire, nous installerons l’ensemble suivant de packages NuGet dans le projet :
Azure.AI.OpenAIMicrosoft.Extensions.AIMicrosoft.Extensions.AI.OpenAISmartComponents.LocalEmbeddings
Le premier package nous aidera à nous connecter au Azure OpenAI service (vous pouvez en utiliser un autre si vous préférez). Les deuxième et troisième sont des wrappers qui facilitent la création de clients de chat et leur injection dans les pages de l’application. Le dernier package contient des classes qui vous permettent de créer des intégrations et d’effectuer des comparaisons sémantiques localement (bien qu’il soit parfaitement valable d’utiliser une base de données vectorielle à cette fin).
La prochaine étape consiste à suivre Guide d’installation des composants Telerik Blazor dans le projet pour copier et coller facilement le code affiché dans les sections suivantes. Jetons ensuite un coup d’œil au premier composant intelligent Telerik qui vous aidera à implémenter l’IA dans des composants de type Data Grid.
Le composant de recherche Grid Smart (AI)
Ce Composant intelligent est basé sur Telerik Grille de données Blazor composant, qui vous permet de visualiser des données avec des fonctionnalités utiles telles que le filtrage, le tri, la pagination, etc. Voyons comment transformer une grille de données en une recherche Grid Smart (AI).
Création d’une page avec recherche de texte
Supposons que dans le projet, nous devions afficher les produits d’une boutique en ligne, en récupérant les produits à partir d’une API REST. Ci-dessous, je vais vous montrer les fichiers que vous devez créer pour suivre cette démo :
ProductSearch.razor (composant de page)
@page "/product-search"
<PageTitle>Product Catalog</PageTitle>
@inject ProductCatalogService productService
<div class="demo-alert demo-alert-info">
<p>Browse our complete product catalog with <strong>@TotalProducts products</strong> across different categories!</p>
</div>
<TelerikGrid @ref="@GridRef"
Data=@GridData
Height="700px"
Pageable=true
PageSize="12"
Sortable="true">
<GridToolBarTemplate>
<div class="search-container">
<TelerikSvgIcon Icon="@SvgIcon.Search" Class="search-icon"></TelerikSvgIcon>
Product Search
<TelerikTextBox @bind-Value="@FilterValue" Placeholder="Search by name, description, category, price..." Class="search-input">
</TelerikTextBox>
<TelerikButton OnClick="@Search" Icon="@SvgIcon.Search" ThemeColor="@ThemeConstants.Button.ThemeColor.Primary">Search</TelerikButton>
<TelerikButton OnClick="@ClearSearch" Icon="@SvgIcon.X" ThemeColor="@ThemeConstants.Button.ThemeColor.Secondary">Clear</TelerikButton>
</div>
</GridToolBarTemplate>
<GridColumns>
<GridColumn Field=@nameof(ProductCatalogDto.ImageUrl) Title="Image" Width="100px" Sortable="false">
<Template>
@{
var product = context as ProductCatalogDto;
}
<div class="product-image-container">
<img src="@product.ImageUrl" alt="@product.Title" class="product-image" />
</div>
</Template>
</GridColumn>
<GridColumn Field=@nameof(ProductCatalogDto.Title) Title="Product" Width="250px">
<Template>
@{
var product = context as ProductCatalogDto;
}
<div class="product-info">
<strong class="product-title">@product.Title</strong>
<div class="product-category">
<TelerikSvgIcon Icon="@SvgIcon.InfoCircle"></TelerikSvgIcon>
@product.CategoryFormatted
</div>
</div>
</Template>
</GridColumn>
<GridColumn Field=@nameof(ProductCatalogDto.ShortDescription) Title="Description" Width="300px">
<Template>
@{
var product = context as ProductCatalogDto;
}
<div class="product-description">
@product.ShortDescription
</div>
</Template>
</GridColumn>
<GridColumn Field=@nameof(ProductCatalogDto.Price) Title="Price" Width="120px">
<Template>
@{
var product = context as ProductCatalogDto;
}
<div class="product-price">
<TelerikSvgIcon Icon="@SvgIcon.Dollar"></TelerikSvgIcon>
<span class="price-value">@product.DisplayPrice</span>
</div>
</Template>
</GridColumn>
<GridColumn Field=@nameof(ProductCatalogDto.Rating.Rate) Title="Rating" Width="140px">
<Template>
@{
var product = context as ProductCatalogDto;
}
<div class="product-rating">
<div class="stars">@product.StarRating</div>
<div class="rating-details">
<span class="rating-value">@product.Rating.Rate.ToString("F1")</span>
<span class="rating-count">(@product.Rating.Count)</span>
</div>
</div>
</Template>
</GridColumn>
<GridColumn Field=@nameof(ProductCatalogDto.Id) Title="Actions" Width="120px" Sortable="false">
<Template>
@{
var product = context as ProductCatalogDto;
}
<div class="product-actions">
<TelerikButton Icon="@SvgIcon.Cart"
Size="@ThemeConstants.Button.Size.Small"
ThemeColor="@ThemeConstants.Button.ThemeColor.Success"
OnClick="@(() => AddToCart(product))"
Title="Add to Cart">
</TelerikButton>
</div>
</Template>
</GridColumn>
</GridColumns>
</TelerikGrid>
@if (IsLoading)
{
<div class="loading-overlay">
<div class="loading-content">
<TelerikLoader Visible="true"
Size="@ThemeConstants.Loader.Size.Large"
ThemeColor="@ThemeConstants.Loader.ThemeColor.Primary"></TelerikLoader>
<p class="loading-text">Loading product catalog...</p>
</div>
</div>
}
@code {
private string FilterValue { get; set; } = "";
private bool IsLoading { get; set; } = true;
private int TotalProducts { get; set; } = 0;
public TelerikGrid<ProductCatalogDto>? GridRef { get; set; }
public IEnumerable<ProductCatalogDto> GridData { get; set; } = new List<ProductCatalogDto>();
public IEnumerable<ProductCatalogDto> AllProducts { get; set; } = new List<ProductCatalogDto>();
protected override async Task OnInitializedAsync()
{
IsLoading = true;
StateHasChanged();
try
{
AllProducts = await productService.GetAllProductsAsync();
var allProductsJson = System.Text.Json.JsonSerializer.Serialize(AllProducts, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
GridData = AllProducts;
TotalProducts = AllProducts.Count();
}
catch (Exception ex)
{
Console.WriteLine($"Error loading products: {ex.Message}");
}
finally
{
IsLoading = false;
StateHasChanged();
}
}
private async Task Search()
{
if (string.IsNullOrEmpty(FilterValue))
{
await ClearSearch();
return;
}
IsLoading = true;
StateHasChanged();
try
{
var searchResults = PerformTextSearch(FilterValue);
GridData = searchResults;
}
finally
{
IsLoading = false;
StateHasChanged();
}
}
private async Task ClearSearch()
{
FilterValue = "";
GridData = AllProducts;
StateHasChanged();
await Task.CompletedTask;
}
private List<ProductCatalogDto> PerformTextSearch(string query)
{
var searchTerm = query.ToLowerInvariant();
return AllProducts.Where(p =>
p.Title.ToLowerInvariant().Contains(searchTerm) ||
p.Description.ToLowerInvariant().Contains(searchTerm) ||
p.CategoryFormatted.ToLowerInvariant().Contains(searchTerm) ||
p.Price.ToString().Contains(searchTerm) ||
p.DisplayPrice.ToLowerInvariant().Contains(searchTerm)
).ToList();
}
private void AddToCart(ProductCatalogDto product)
{
Console.WriteLine($"Added {product.Title} to cart - ${product.Price}");
}
}
<style>
.demo-alert.demo-alert-info {
margin: 5px auto 15px;
padding: 15px 20px;
background-color: #e3f2fd;
border: 1px solid #2196f3;
border-radius: 4px;
color: #1565c0;
}
.toolbar-container {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
font-size: 1.1em;
color: #333;
}
.toolbar-icon {
color: #666;
font-size: 1.2em;
}
.search-container {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
padding: 10px 0;
}
.search-icon {
color: #666;
}
.search-input {
min-width: 400px;
flex: 1;
}
.product-image-container {
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
}
.product-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #ddd;
}
.product-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.product-title {
font-size: 1em;
color: #333;
line-height: 1.3;
}
.product-category {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.85em;
color: #666;
background-color: #f5f5f5;
padding: 2px 6px;
border-radius: 12px;
width: fit-content;
}
.product-description {
color: #555;
font-size: 0.9em;
line-height: 1.4;
}
.product-price {
display: flex;
align-items: center;
gap: 5px;
font-weight: bold;
color: #1976d2;
font-size: 1.1em;
}
.price-value {
font-family: monospace;
}
.product-rating {
display: flex;
flex-direction: column;
gap: 3px;
}
.stars {
color: #ffc107;
font-size: 1.1em;
letter-spacing: 1px;
}
.rating-details {
font-size: 0.85em;
color: #666;
}
.rating-value {
font-weight: bold;
color: #333;
}
.rating-count {
color: #888;
}
.product-actions {
display: flex;
gap: 5px;
justify-content: center;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(2px);
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.loading-text {
margin: 0;
font-size: 1.1rem;
color: #666;
font-weight: 500;
}
</style>
ProductCatalogDto.cs – Modèle de produit
using System.Text.Json.Serialization;
namespace TelerikSmartComponentsDemo.Models;
public class ProductCatalogDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("price")]
public decimal Price { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("category")]
public string Category { get; set; } = string.Empty;
[JsonPropertyName("image")]
public string ImageUrl { get; set; } = string.Empty;
[JsonPropertyName("rating")]
public ProductRatingDto Rating { get; set; } = new();
public string DisplayPrice => $"${Price:F2}";
public string ShortDescription => Description.Length > 100 ? Description.Substring(0, 100) + "..." : Description;
public string CategoryFormatted => Category.Replace("'", "").Replace(" ", " ").ToTitleCase();
public string SearchText => $"{Title} {Description} {Category} ${Price} {Rating.Rate} stars";
public string StarRating => new string('★', (int)Math.Round(Rating.Rate)) + new string('☆', 5 - (int)Math.Round(Rating.Rate));
}
public class ProductRatingDto
{
[JsonPropertyName("rate")]
public double Rate { get; set; }
[JsonPropertyName("count")]
public int Count { get; set; }
}
public static class StringExtensions
{
public static string ToTitleCase(this string input)
{
if (string.IsNullOrEmpty(input))
return input;
var words = input.Split(' ');
for (int i = 0; i < words.Length; i++)
{
if (words[i].Length > 0)
{
words[i] = char.ToUpper(words[i][0]) + (words[i].Length > 1 ? words[i].Substring(1).ToLower() : "");
}
}
return string.Join(" ", words);
}
}
ProductCatalogService.cs – Service de récupération des produits
public class ProductCatalogService
{
private readonly HttpClient _httpClient;
private readonly ILogger<ProductCatalogService> _logger;
private List<ProductCatalogDto>? _cachedProducts;
public ProductCatalogService(HttpClient httpClient, ILogger<ProductCatalogService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<IEnumerable<ProductCatalogDto>> GetAllProductsAsync()
{
if (_cachedProducts != null)
{
_logger.LogInformation("Returning cached product data");
return _cachedProducts;
}
try
{
_logger.LogInformation("Fetching products from Fake Store API...");
var response = await _httpClient.GetAsync("https://fakestoreapi.com/products");
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var products = JsonSerializer.Deserialize<List<ProductCatalogDto>>(jsonContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (products != null && products.Any())
{
_cachedProducts = products;
_logger.LogInformation($"Successfully loaded {products.Count} products from API");
return products;
}
}
else
{
_logger.LogWarning($"API request failed with status: {response.StatusCode}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching products from Fake Store API");
}
_logger.LogInformation("Using mock product data as fallback");
return GetMockProducts();
}
public async Task<IEnumerable<ProductCatalogDto>> GetProductsByCategoryAsync(string category)
{
try
{
_logger.LogInformation($"Fetching products for category: {category}");
var response = await _httpClient.GetAsync($"https://fakestoreapi.com/products/category/{category}");
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var products = JsonSerializer.Deserialize<List<ProductCatalogDto>>(jsonContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (products != null && products.Any())
{
_logger.LogInformation($"Successfully loaded {products.Count} products for category {category}");
return products;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error fetching products for category {category}");
}
var allMockProducts = GetMockProducts();
return allMockProducts.Where(p => p.Category.Equals(category, StringComparison.OrdinalIgnoreCase));
}
public async Task<IEnumerable<string>> GetCategoriesAsync()
{
try
{
var response = await _httpClient.GetAsync("https://fakestoreapi.com/products/categories");
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var categories = JsonSerializer.Deserialize<List<string>>(jsonContent);
if (categories != null && categories.Any())
{
return categories.Select(c => c.ToTitleCase());
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching categories");
}
return new[] { "Electronics", "Jewelery", "Men's Clothing", "Women's Clothing" };
}
private List<ProductCatalogDto> GetMockProducts()
{
return new List<ProductCatalogDto>
{
new ProductCatalogDto
{
Id = 1,
Title = "iPhone 15 Pro Max",
Price = 1199.99m,
Description = "The latest iPhone with advanced camera system, A17 Pro chip, and titanium design. Features 6.7-inch Super Retina XDR display.",
Category = "electronics",
ImageUrl = "https://via.placeholder.com/300x300?text=iPhone+15",
Rating = new ProductRatingDto { Rate = 4.8, Count = 1250 }
},
new ProductCatalogDto
{
Id = 2,
Title = "Samsung Galaxy S24 Ultra",
Price = 1299.99m,
Description = "Premium Android smartphone with S Pen, 200MP camera, and AI-powered features. 6.8-inch Dynamic AMOLED display.",
Category = "electronics",
ImageUrl = "https://via.placeholder.com/300x300?text=Galaxy+S24",
Rating = new ProductRatingDto { Rate = 4.7, Count = 980 }
},
new ProductCatalogDto
{
Id = 3,
Title = "MacBook Pro 16-inch M3",
Price = 2499.99m,
Description = "Professional laptop with M3 chip, 16-inch Liquid Retina XDR display, and up to 22 hours of battery life.",
Category = "electronics",
ImageUrl = "https://via.placeholder.com/300x300?text=MacBook+Pro",
Rating = new ProductRatingDto { Rate = 4.9, Count = 750 }
},
new ProductCatalogDto
{
Id = 4,
Title = "Sony WH-1000XM5 Headphones",
Price = 399.99m,
Description = "Industry-leading noise canceling wireless headphones with 30-hour battery life and crystal clear hands-free calling.",
Category = "electronics",
ImageUrl = "https://via.placeholder.com/300x300?text=Sony+Headphones",
Rating = new ProductRatingDto { Rate = 4.6, Count = 2100 }
},
new ProductCatalogDto
{
Id = 5,
Title = "Gold Diamond Ring",
Price = 899.99m,
Description = "Elegant 14k gold ring with natural diamonds. Perfect for engagements or special occasions. Comes with certificate of authenticity.",
Category = "jewelery",
ImageUrl = "https://via.placeholder.com/300x300?text=Diamond+Ring",
Rating = new ProductRatingDto { Rate = 4.5, Count = 145 }
},
new ProductCatalogDto
{
Id = 6,
Title = "Men's Casual Cotton T-Shirt",
Price = 29.99m,
Description = "Comfortable 100% cotton t-shirt in various colors. Perfect for everyday wear with a relaxed fit and soft fabric.",
Category = "men's clothing",
ImageUrl = "https://via.placeholder.com/300x300?text=Cotton+Tshirt",
Rating = new ProductRatingDto { Rate = 4.2, Count = 890 }
},
new ProductCatalogDto
{
Id = 7,
Title = "Women's Summer Dress",
Price = 79.99m,
Description = "Elegant floral summer dress made from breathable fabric. Features adjustable straps and a flattering A-line silhouette.",
Category = "women's clothing",
ImageUrl = "https://via.placeholder.com/300x300?text=Summer+Dress",
Rating = new ProductRatingDto { Rate = 4.4, Count = 567 }
},
new ProductCatalogDto
{
Id = 8,
Title = "Wireless Gaming Mouse",
Price = 89.99m,
Description = "High-precision wireless gaming mouse with RGB lighting, programmable buttons, and 100-hour battery life.",
Category = "electronics",
ImageUrl = "https://via.placeholder.com/300x300?text=Gaming+Mouse",
Rating = new ProductRatingDto { Rate = 4.3, Count = 1200 }
}
};
}
}
Ensuite, pour éviter les exceptions, nous devons enregistrer le service de récupération de produits dans Program.csainsi que HttpClient comme suit:
...
builder.Services.AddScoped<ProductCatalogService>();
builder.Services.AddHttpClient();
var app = builder.Build();
...
Une fois ce qui précède configuré, nous pourrons voir l’application en action. Cette application peut effectuer des recherches textuelles mais pas des recherches sémantiques, comme le montre l’image suivante :
Dans l’image précédente, vous pouvez voir comment il est possible d’effectuer une recherche textuelle de produits avec les termes jeu et disquequi renvoie des résultats. Cependant, lorsque l’on tente d’effectuer une recherche sémantique avec le terme stockage (qui est un synonyme sémantique de disque), aucune correspondance n’est affichée. Voyons comment améliorer cela.
Création d’intégrations de produits
Pour effectuer une recherche sémantique sur notre page, il est nécessaire de travailler avec des intégrations. Heureusement, le SmartComponents.LocalEmbeddings Le package contient tout le nécessaire pour travailler avec eux. Pour cette raison, dans la section code du projet, nous ajouterons une nouvelle propriété de dictionnaire qui mappe le Id de chaque produit à son intégration respective, comme suit :
@code {
..
Dictionary<int, EmbeddingF32>();
protected override async Task OnInitializedAsync()
{
..
Le EmbeddingF32 La classe permet de stocker des intégrations dans la mémoire locale, ainsi que d’effectuer des opérations de comparaison de similarité qui pourraient être utiles pour regrouper des produits similaires, générer des recommandations ou, dans notre cas, rechercher des éléments en fonction d’un terme de recherche.
Une fois que nous avons la propriété qui nous permettra de stocker les intégrations de produits, l’étape suivante consiste à extraire les informations de chaque produit et à les convertir en intégration. Nous le ferons une fois les produits récupérés du service REST, comme le montre l’exemple suivant :
try
{
...
GridData = AllProducts;
using var embedder = new LocalEmbedder();
foreach (var product in AllProducts)
{
ProductEmbeddings.Add(product.Id, embedder.Embed(product.SearchText));
}
...
}
Dans le code précédent, un nouvel élément est ajouté à la liste des plongements, avec le produit Id comme clé. Ensuite, celui de l’intégrateur Embed méthode est utilisée pour transformer le produit SearchText propriété dans une intégration, qui est stockée en tant que valeur de l’élément dans le dictionnaire. Pour convertir toutes les informations sur le produit telles que Description, Category, Ratingetc. dans une intégration, le SearchText La propriété a été définie dans le modèle de produit, qui concatène toutes les informations comme indiqué ci-dessous :
public string SearchText => $"{Title} {Description} {Category} ${Price} {Rating.Rate} stars";
Voyons maintenant comment effectuer des recherches sémantiques.
Effectuer des recherches sémantiques dans une recherche Smart Grid (AI)
Une fois que nous avons toutes les informations sémantiques des produits en mémoire, il est temps de remplacer le contenu du PerformTextSearch méthode, afin d’éliminer l’utilisation de la Contains méthode qui effectue uniquement des recherches textuelles.
La première chose que nous ferons est de créer un nouveau LocalEmbedder afin de réutiliser le Embed méthode, cette fois pour obtenir l’incorporation du terme de recherche :
private List<ProductCatalogDto> PerformTextSearch(string query)
{
using var embedder = new LocalEmbedder();
var queryVector = embedder.Embed(query);
}
Ensuite, à des fins de démonstration et pour vous permettre d’effectuer des tests, nous parcourrons chaque produit pour voir sa valeur de similarité par rapport au terme de recherche dans le Output fenêtre comme suit :
...
foreach (var product in AllProducts)
{
var similarity = LocalEmbedder.Similarity(ProductEmbeddings[product.Id], queryVector);
Debug.WriteLine($"{product.Title} is {similarity:F3} similar to query: {query}");
}
...
Enfin, nous effectuerons un filtrage et un tri en fonction de la comparaison de similarité, en ajoutant un seuil qui indique le degré de similarité que nous appliquerons. Ce résultat sera ce que nous renvoyons dans la méthode :
...
var threshold = 0.5;
return AllProducts
.Where(p => LocalEmbedder.Similarity(ProductEmbeddings[p.Id], queryVector) > threshold)
.OrderByDescending(p => LocalEmbedder.Similarity(ProductEmbeddings[p.Id], queryVector))
.ToList();
...
Lors de l’exécution de la page, nous aurons un résultat comme celui-ci :
Dans l’exemple précédent, vous pouvez voir qu’une recherche sémantique est désormais en cours, car la saisie du terme stockage trouve des produits liés non seulement aux disques durs mais aussi aux sacs à dos.
Le composant Smart AI Search ComboBox
Un autre composant qui est également très utile et peut devenir un composant intelligent est le Telerik ComboBox pour Blazor. Pour y parvenir, nous suivrons un flux similaire à ce que nous avons vu avec le composant Grid Smart (AI) Search. Ci-dessous, je vais vous montrer les classes pour créer une page où un utilisateur peut obtenir certaines villes avec leur météo, afin d’effectuer ultérieurement une recherche sémantique :
MétéoDto.cs – Maquette d’une ville avec sa météo
public class WeatherDto
{
[JsonPropertyName("name")]
public string CityName { get; set; } = string.Empty;
[JsonPropertyName("main")]
public WeatherMainDto Main { get; set; } = new();
[JsonPropertyName("weather")]
public List<WeatherDescriptionDto> Weather { get; set; } = new();
[JsonPropertyName("wind")]
public WeatherWindDto Wind { get; set; } = new();
[JsonPropertyName("sys")]
public WeatherSysDto Sys { get; set; } = new();
[JsonPropertyName("visibility")]
public int Visibility { get; set; }
[JsonPropertyName("dt")]
public long DateTime { get; set; }
public string DisplayName => $"{CityName}, {Sys.Country}";
public string Description => Weather.FirstOrDefault()?.Description ?? "No description";
public double Temperature => Main.Temp;
public string SearchText => $"{CityName} {Sys.Country} {Description} {Main.Temp}°C weather climate";
}
public class WeatherMainDto
{
[JsonPropertyName("temp")]
public double Temp { get; set; }
[JsonPropertyName("feels_like")]
public double FeelsLike { get; set; }
[JsonPropertyName("temp_min")]
public double TempMin { get; set; }
[JsonPropertyName("temp_max")]
public double TempMax { get; set; }
[JsonPropertyName("pressure")]
public int Pressure { get; set; }
[JsonPropertyName("humidity")]
public int Humidity { get; set; }
}
public class WeatherDescriptionDto
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("main")]
public string Main { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("icon")]
public string Icon { get; set; } = string.Empty;
}
public class WeatherWindDto
{
[JsonPropertyName("speed")]
public double Speed { get; set; }
[JsonPropertyName("deg")]
public int Deg { get; set; }
}
public class WeatherSysDto
{
[JsonPropertyName("country")]
public string Country { get; set; } = string.Empty;
[JsonPropertyName("sunrise")]
public long Sunrise { get; set; }
[JsonPropertyName("sunset")]
public long Sunset { get; set; }
}
MétéoService.cs – Service de récupération météo
Dans ce fichier, vous pouvez ajouter votre propre clé si vous souhaitez obtenir des données réelles, ou travailler avec des données fixes :
public class WeatherService
{
private readonly HttpClient _http;
private readonly Dictionary<string, WeatherDto> _cachedWeather = new();
private readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(10);
private const string API_KEY = "YOUR_OPENWEATHER_API_KEY_HERE";
private const string BASE_URL = "https://api.openweathermap.org/data/2.5";
public WeatherService(HttpClient http)
{
_http = http;
}
public async Task<IEnumerable<WeatherDto>> GetWeatherForMultipleCitiesAsync()
{
var cities = new[] { "Reykjavik", "Singapore", "Seattle", "Cairo", "Bariloche", "Dubai", "Bali", "Tromsø", "Lima", "San Diego" };
var weatherData = new List<WeatherDto>();
foreach (var city in cities)
{
var weather = await GetWeatherByCityAsync(city);
if (weather != null)
{
weatherData.Add(weather);
}
}
return weatherData;
}
public async Task<WeatherDto?> GetWeatherByCityAsync(string city)
{
var cacheKey = $"{city}_{DateTime.Now:yyyy-MM-dd-HH}";
if (_cachedWeather.ContainsKey(cacheKey))
{
return _cachedWeather[cacheKey];
}
try
{
if (API_KEY == "YOUR_OPENWEATHER_API_KEY_HERE")
{
return GetMockWeatherForCity(city);
}
var url = $"{BASE_URL}/weather?q={Uri.EscapeDataString(city)}&appid={API_KEY}&units=metric";
var weather = await _http.GetFromJsonAsync<WeatherDto>(url);
if (weather != null)
{
_cachedWeather[cacheKey] = weather;
return weather;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error fetching weather for {city}: {ex.Message}");
return GetMockWeatherForCity(city);
}
return null;
}
private WeatherDto GetMockWeatherForCity(string city)
{
var random = new Random(city.GetHashCode());
var weatherTypes = new[]
{
("Clear", "clear sky"),
("Clouds", "few clouds"),
("Clouds", "scattered clouds"),
("Rain", "light rain"),
("Snow", "light snow")
};
var weatherType = weatherTypes[random.Next(weatherTypes.Length)];
var countries = new Dictionary<string, string>
{
{ "New York", "US" }, { "London", "GB" }, { "Tokyo", "JP" },
{ "Paris", "FR" }, { "Sydney", "AU" }, { "Berlin", "DE" },
{ "Madrid", "ES" }, { "Rome", "IT" }, { "Amsterdam", "NL" }, { "Stockholm", "SE" }
};
return new WeatherDto
{
CityName = city,
Main = new WeatherMainDto
{
Temp = Math.Round(random.NextDouble() * 35 - 5, 1),
FeelsLike = Math.Round(random.NextDouble() * 35 - 5, 1),
TempMin = Math.Round(random.NextDouble() * 30 - 10, 1),
TempMax = Math.Round(random.NextDouble() * 40 + 5, 1),
Humidity = random.Next(30, 90),
Pressure = random.Next(990, 1030)
},
Weather = new List<WeatherDescriptionDto>
{
new WeatherDescriptionDto
{
Main = weatherType.Item1,
Description = weatherType.Item2,
Icon = "01d"
}
},
Wind = new WeatherWindDto
{
Speed = Math.Round(random.NextDouble() * 15, 1),
Deg = random.Next(0, 360)
},
Sys = new WeatherSysDto
{
Country = countries.GetValueOrDefault(city, "XX"),
Sunrise = DateTimeOffset.Now.ToUnixTimeSeconds() - 6 * 3600,
Sunset = DateTimeOffset.Now.ToUnixTimeSeconds() + 6 * 3600
},
Visibility = random.Next(5000, 10000),
DateTime = DateTimeOffset.Now.ToUnixTimeSeconds()
};
}
}
De plus, n’oubliez pas d’ajouter le WeatherService conteneur de dépendances dans Program.cs:
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddScoped<WeatherService>();
...
var app = builder.Build();
Enfin, ajoutez une nouvelle page à votre projet. Dans mon cas, cela s’appelle WeatherSmartAISearch.razorqui se présente comme suit :
@page "/weather-smart-ai-search"
<PageTitle>Weather Smart AI Search Demo</PageTitle>
@inject WeatherService weatherService
<div class="demo-alert demo-alert-info">
<p>Try typing <strong>"sunny"</strong>, <strong>"cold"</strong>, <strong>"rain"</strong>, or <strong>"warm weather"</strong> in the combobox to see how our smart search works with weather data!</p>
</div>
<div class="weather-search-container">
<h4>Find Cities by Weather Conditions</h4>
<TelerikComboBox @bind-Value="@SelectedWeatherId"
TItem="@WeatherDto" TValue="@string"
OnRead="@ReadWeatherItems"
Placeholder="Search for weather conditions (e.g., sunny, rainy, cold)"
ValueField="@nameof(WeatherDto.CityName)"
TextField="@nameof(WeatherDto.DisplayName)"
Filterable="true"
ShowClearButton="true"
Width="400px">
<ItemTemplate>
@{
var weather = context as WeatherDto;
}
<div class="weather-item">
<div class="weather-city">
<TelerikSvgIcon Icon="@SvgIcon.MapMarker"></TelerikSvgIcon>
@weather.DisplayName
</div>
<div class="weather-details">
<span class="weather-temp">@weather.Temperature.ToString("F1")°C</span>
<span class="weather-desc">@weather.Description</span>
</div>
</div>
</ItemTemplate>
</TelerikComboBox>
@if (!string.IsNullOrEmpty(SelectedWeatherId) && SelectedWeather != null)
{
<div class="weather-details-card">
<TelerikCard>
<CardHeader>
<CardTitle>
<TelerikSvgIcon Icon="@SvgIcon.Cloud"></TelerikSvgIcon>
Weather in @SelectedWeather.DisplayName
</CardTitle>
</CardHeader>
<CardBody>
<div class="weather-info-grid">
<div class="weather-info-item">
<TelerikSvgIcon Icon="@SvgIcon.InfoCircle"></TelerikSvgIcon>
<span class="label">Temperature:</span>
<span class="value">@SelectedWeather.Temperature.ToString("F1")°C</span>
</div>
<div class="weather-info-item">
<TelerikSvgIcon Icon="@SvgIcon.Eye"></TelerikSvgIcon>
<span class="label">Feels like:</span>
<span class="value">@SelectedWeather.Main.FeelsLike.ToString("F1")°C</span>
</div>
<div class="weather-info-item">
<TelerikSvgIcon Icon="@SvgIcon.Droplet"></TelerikSvgIcon>
<span class="label">Humidity:</span>
<span class="value">@SelectedWeather.Main.Humidity%</span>
</div>
<div class="weather-info-item">
<TelerikSvgIcon Icon="@SvgIcon.InfoCircle"></TelerikSvgIcon>
<span class="label">Pressure:</span>
<span class="value">@SelectedWeather.Main.Pressure hPa</span>
</div>
<div class="weather-info-item">
<TelerikSvgIcon Icon="@SvgIcon.InfoCircle"></TelerikSvgIcon>
<span class="label">Wind:</span>
<span class="value">@SelectedWeather.Wind.Speed.ToString("F1") m/s</span>
</div>
<div class="weather-info-item">
<TelerikSvgIcon Icon="@SvgIcon.Cloud"></TelerikSvgIcon>
<span class="label">Condition:</span>
<span class="value">@SelectedWeather.Description</span>
</div>
</div>
</CardBody>
</TelerikCard>
</div>
}
</div>
@if (IsLoading)
{
<div class="loading-container">
<div class="loading-content">
<TelerikLoader Visible="true"
Size="@ThemeConstants.Loader.Size.Large"
ThemeColor="@ThemeConstants.Loader.ThemeColor.Primary"></TelerikLoader>
<p class="loading-text">Loading weather data...</p>
</div>
</div>
}
@code {
private string SelectedWeatherId { get; set; } = "";
private bool IsLoading { get; set; } = true;
public IEnumerable<WeatherDto> AllWeatherData { get; set; } = new List<WeatherDto>();
public Dictionary<string, EmbeddingF32> WeatherEmbeddings { get; set; } = new Dictionary<string, EmbeddingF32>();
public WeatherDto? SelectedWeather => AllWeatherData.FirstOrDefault(w => w.CityName == SelectedWeatherId);
protected override async Task OnInitializedAsync()
{
IsLoading = true;
StateHasChanged();
try
{
AllWeatherData = await weatherService.GetWeatherForMultipleCitiesAsync();
using var embedder = new LocalEmbedder();
foreach (var weather in AllWeatherData)
{
WeatherEmbeddings.Add(weather.CityName, embedder.Embed(weather.SearchText));
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading weather data: {ex.Message}");
}
finally
{
IsLoading = false;
StateHasChanged();
}
}
protected void ReadWeatherItems(ComboBoxReadEventArgs args)
{
if (args.Request.Filters.Count > 0)
{
var filter = args.Request.Filters[0] as Telerik.DataSource.FilterDescriptor;
string userInput = filter?.Value?.ToString() ?? "";
if (!string.IsNullOrEmpty(userInput))
{
var searchResults = PerformSmartWeatherSearch(userInput);
args.Data = searchResults;
}
else
{
args.Data = AllWeatherData;
}
}
else
{
args.Data = AllWeatherData;
}
}
public List<WeatherDto> PerformSmartWeatherSearch(string query)
{
using var embedder = new LocalEmbedder();
var queryVector = embedder.Embed(query);
foreach (var weather in AllWeatherData)
{
var similarity = LocalEmbedder.Similarity(WeatherEmbeddings[weather.CityName], queryVector);
Console.WriteLine($"{weather.DisplayName} ({weather.Description}) is {similarity:F3} similar to query: {query}");
}
var threshold = 0.6;
return AllWeatherData
.Where(w => LocalEmbedder.Similarity(WeatherEmbeddings[w.CityName], queryVector) > threshold)
.OrderByDescending(w => LocalEmbedder.Similarity(WeatherEmbeddings[w.CityName], queryVector))
.ToList();
}
}
<style>
.demo-alert.demo-alert-info {
margin: 5px auto 15px;
padding: 15px 20px;
background-color: #e3f2fd;
border: 1px solid #2196f3;
border-radius: 4px;
color: #1565c0;
}
.weather-search-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.weather-search-container h4 {
margin-bottom: 20px;
color: #333;
}
.weather-item {
padding: 8px 0;
}
.weather-city {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.weather-details {
display: flex;
gap: 15px;
font-size: 0.9em;
color: #666;
margin-left: 24px;
}
.weather-temp {
font-weight: 500;
color: #1976d2;
}
.weather-desc {
font-style: italic;
}
.weather-details-card {
margin-top: 30px;
}
.weather-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.weather-info-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background-color: #f8f9fa;
border-radius: 4px;
}
.weather-info-item .label {
font-weight: 500;
color: #555;
}
.weather-info-item .value {
color: #1976d2;
font-weight: 500;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.loading-text {
margin: 0;
font-size: 1.1rem;
color: #666;
font-weight: 500;
}
</style>
Avec le code modifié, le résultat est le suivant :
Au sein du composant, vous pouvez remarquer que le flux suivant est suivi :
- Une propriété de dictionnaire de type
Dictionary<string, EmbeddingF32>est créé. - Pour chaque résultat météo, une intégration locale est créée et ajoutée au dictionnaire d’intégrations.
- Lors de l’exécution d’une recherche, l’intégration du texte à rechercher est créée et une requête de similarité est exécutée pour renvoyer des résultats similaires.
Maintenant, travaillons avec Assistant IA intelligent PDFViewer.
Le composant PDFViewer Smart AI Assistant
Le dernier élément que nous examinerons est le Assistant IA intelligent PDFViewerce qui est utile pour aider les utilisateurs à répondre aux questions qu’ils peuvent se poser sur un document PDF. Pour que ce contrôle fonctionne correctement, vous devez ajouter un modèle d’IA de votre choix pour fonctionner avec le contexte trouvé en fonction des questions que les utilisateurs peuvent avoir.
Commençons par configurer Program.csenregistrement et configuration ChatClient travailler avec un déploiement dans Azure OpenAI:
var key = builder.Configuration["AzureOPENAI:Key"];
if (!string.IsNullOrEmpty(key))
{
builder.Services
.AddChatClient(new AzureOpenAIClient(
new Uri("your-deployment-endpoint"),
new ApiKeyCredential(key))
.GetChatClient("your-deployment-name").AsIChatClient());
}
else
{
Debug.WriteLine("Warning: AzureOPENAI:Key not configured. AI features will not be available.");
}
var app = builder.Build();
Dans le code ci-dessus, la clé de service est obtenue à partir des variables d’environnement, tandis que le point de terminaison de déploiement et son nom sont spécifiés manuellement, bien que vous puissiez le configurer pour obtenir les informations où que vous soyez.
L’étape suivante consiste à ajouter un document PDF contenant du texte à votre projet. Dans mon exemple, je l’ai ajouté au wwwroot dossier à des fins de chargement local. Bien que je le fasse de cette façon, vous pouvez le charger à partir d’une URL externe, lire un document généré dynamiquement, etc.
Ensuite, créez un nouveau composant de type page, dans mon cas, je l’ai appelé PdfViewerSmart.razor avec le code suivant :
@page "/pdfviewer"
@inject IChatClient ChatClient
@inject NavigationManager NavigationManager
@inject HttpClient Http
<PageTitle>PdfViewer Smart AI Assistant Demo</PageTitle>
@inject IJSRuntime jsRuntime
<div class="demo-alert demo-alert-info">
<p>To run the demo configure your AI API credentials inside <code>CallOpenAIApi()</code> method.</p>
</div>
<TelerikPdfViewer @ref="@PdfViewerRef"
Width="100%"
Height="800px"
Data="@FileData">
<PdfViewerToolBar>
<PdfViewerToolBarCustomTool>
<TelerikButton Id="ai-button" OnClick="@ToggleAIPrompt" Icon="SvgIcon.Sparkles">AI Assistant</TelerikButton>
<TelerikPopup @ref="@PopupRef"
AnimationDuration="300"
AnimationType="@AnimationType.SlideUp"
AnchorHorizontalAlign="@PopupAnchorHorizontalAlign.Left"
AnchorVerticalAlign="@PopupAnchorVerticalAlign.Bottom"
AnchorSelector="#ai-button"
HorizontalAlign="@PopupHorizontalAlign.Left"
VerticalAlign="@PopupVerticalAlign.Top"
Class="ai-prompt-popup"
Width="420px"
Height="400px">
<TelerikAIPrompt OnPromptRequest="@HandlePromptRequest"
PromptSuggestions="@PromptSuggestions">
</TelerikAIPrompt>
</TelerikPopup>
</PdfViewerToolBarCustomTool>
</PdfViewerToolBar>
</TelerikPdfViewer>
@code {
public TelerikPdfViewer PdfViewerRef { get; set; }
public byte[] FileData { get; set; }
public List<string> PromptSuggestions { get; set; }
public bool IsAIPromptPopupVisible { get; set; }
public TelerikPopup PopupRef { get; set; }
public Dictionary<string, EmbeddingF32> PageEmbeddings { get; set; }
public string[] TextInChunks { get; set; }
bool firstRender = true;
private void ToggleAIPrompt()
{
if (IsAIPromptPopupVisible)
{
PopupRef.Hide();
}
else
{
PopupRef.Show();
}
IsAIPromptPopupVisible = !IsAIPromptPopupVisible;
}
private async Task HandlePromptRequest(AIPromptPromptRequestEventArgs args)
{
var prompt = args.Prompt;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var url = NavigationManager.ToAbsoluteUri("AI Prompt Docs.pdf");
FileData = await Http.GetByteArrayAsync(url);
StateHasChanged();
}
}
}
Le code ci-dessus sert à avoir une page de base qui charge le document PDF que nous avons ajouté lors d’une étape précédente. Vous remarquerez peut-être que dans le cadre des outils du PDFViewer un TelerikPopup a été ajouté, qui à son tour contient un TelerikAIPrompt à l’intérieur. Cet outil personnalisé nous permettra de poser des questions sur le document, auxquelles le modèle d’IA répondra en fonction d’un contexte.
L’idée est que lors du chargement de la page, le document est divisé en morceaux afin que lorsque l’utilisateur pose une question, il ne reçoive que les informations les plus pertinentes en fonction de leur similarité. Il existe différentes stratégies pour y parvenir, mais dans notre exemple, nous diviserons le texte du document en morceaux de 500 caractères, plus 100 caractères précédents et 100 caractères suivants pour éviter de perdre trop de contexte entre les morceaux. Nous ferons cela en définissant une fonction JavaScript comme indiqué ci-dessous :
<script suppress-error="BL9992">
window.getLoadedDocumentText = async () => {
const findViewer = () => {
const instances = TelerikBlazor._instances || {};
for (const [_, instance] of Object.entries(instances)) {
if (instance.element?.classList.contains("k-pdf-viewer")) {
return instance;
}
}
return null;
};
let pdfInstance = findViewer();
while (!pdfInstance) {
await new Promise(resolve => setTimeout(resolve, 200));
pdfInstance = findViewer();
}
let pdfDocument = pdfInstance.widget.state.pdfDocument;
while (!pdfDocument) {
await new Promise(resolve => setTimeout(resolve, 200));
pdfDocument = pdfInstance.widget.state.pdfDocument;
}
let allText = "";
for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber++) {
const content = await (await pdfDocument.getPage(pageNumber)).getTextContent();
allText += content.items.map(item => item.str).join("");
}
const chunkSize = 500;
const overlap = 100;
const step = chunkSize - overlap;
const chunks = [];
for (let start = 0; start < allText.length; start += step) {
chunks.push(allText.slice(start, start + chunkSize));
}
return chunks;
};
</script>
Le code JS renvoie la liste des chunks, que nous récupérerons du OnAfterRenderAsync méthode une fois qu’on a lu le document :
...
TextInChunks = await jsRuntime.InvokeAsync<string[]>("getLoadedDocumentText");
var allText = string.Join(" --- ", TextInChunks);
var embedder = new LocalEmbedder();
PageEmbeddings = TextInChunks.Select(x => KeyValuePair.Create(x, embedder.Embed(x))).ToDictionary(k => k.Key, v => v.Value);
La prochaine étape consistera à créer une méthode appelée CallOpenAIApi qui permettra d’interroger le modèle d’IA, en passant en paramètres un systemPrompt et un messagerenvoyant uniquement le texte de la réponse. Nous utiliserons cette méthode pour générer une série de propositions de questions initiales pour les utilisateurs, ainsi que pour répondre à leurs questions sur les informations du document :
private async Task<string> CallOpenAIApi(string systemPrompt, string message)
{
var options = new ChatOptions
{
Instructions = systemPrompt
};
var answer = await ChatClient.GetResponseAsync(message, options);
return answer.Text;
}
Une fois cette méthode définie et la liste des intégrations créée, nous générerons une série de suggestions de questions pour l’utilisateur qui seront affichées dans le TelerikAIPrompt composant comme suit :
var questionsJson = await CallOpenAIApi(
@"You are a helpful assistant. Your task is to analyze the provided text and generate 3 short diverse questions.
The questions should be returned in form of a string array in a valid JSON format. Return only the JSON and nothing else without markdown code formatting.
Example output: [""Question 1"", ""Question 2"", ""Question 3""]",
allText);
PromptSuggestions = System.Text.Json.JsonSerializer.Deserialize<List<string>>(questionsJson);
PopupRef.Refresh();
StateHasChanged();
D’autre part, pour répondre aux questions qu’un utilisateur peut se poser, nous appliquerons un flux similaire à celui que nous avons suivi dans les deux composants intelligents précédents, c’est-à-dire convertir la question en intégration, comparer l’intégration avec le dictionnaire des intégrations et renvoyer les éléments les plus proches, comme suit :
private async Task<string> AnswerQuestion(string question)
{
var embedder = new LocalEmbedder();
var questionEmbedding = embedder.Embed(question);
var results = LocalEmbedder.FindClosest(questionEmbedding, PageEmbeddings.Select(x => (x.Key, x.Value)), 2);
string prompt = $"You are a helpful assistant. Use the provided context to answer the user question. Context: {string.Join(" --- ", results)}";
var answer = await CallOpenAIApi(prompt, question);
return answer;
}
Dans le code ci-dessus, vous pouvez remarquer comment le FindClosest La méthode est utilisée pour renvoyer uniquement les articles les plus proches en fonction de la quantité spécifiée. De même, nous modifierons le HandlePromptRequest méthode pour afficher la réponse à l’utilisateur dans le TelerikAIPrompt composant comme indiqué ci-dessous :
private async Task HandlePromptRequest(AIPromptPromptRequestEventArgs args)
{
var prompt = args.Prompt;
var answer = await AnswerQuestion(prompt);
args.Output = answer;
}
Lors de l’exécution de l’application avec les modifications précédentes, nous voyons d’abord les suggestions d’invite initiales générées à partir du document. Vous pouvez également voir qu’après avoir appuyé sur le Générer bouton, une réponse est générée qui a utilisé les morceaux comme base pour répondre avec précision à la question de l’utilisateur :
C’est ainsi que nous avons implémenté Telerik Smart Components dans un projet à partir de zéro.
Conclusion
Tout au long de cet article, vous avez appris ce que sont les composants intelligents Telerik et comment les implémenter dans un projet Blazor à partir de zéro. Vous avez également vu les composants nécessaires à leur fonctionnement, ce qui vous a fourni les outils nécessaires pour intégrer ces nouveaux composants intelligents dans vos propres projets, offrant ainsi à vos utilisateurs de meilleures expériences grâce à l’utilisation de l’IA.
Prêt à essayer l’interface utilisateur Telerik pour Blazor ? Il est livré avec un essai gratuit de 30 jours.
Source link
