mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
RadzenChat component added (#2254)
This commit is contained in:
172
Radzen.Blazor.Tests/AIChatTests.cs
Normal file
172
Radzen.Blazor.Tests/AIChatTests.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using Bunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Radzen;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class AIChatTests
|
||||
{
|
||||
private void RegisterChatService(TestContext ctx)
|
||||
{
|
||||
// Register a dummy HttpClient and default options for AIChatService
|
||||
ctx.Services.AddSingleton(new HttpClient());
|
||||
ctx.Services.AddScoped<IAIChatService, AIChatService>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldRenderWithDefaultProperties()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
Assert.Contains("Chat", component.Markup);
|
||||
Assert.Contains("Type your message...", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldRenderWithCustomTitle()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.Title, "Custom Chat"));
|
||||
Assert.Contains("Custom Chat", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldRenderWithCustomPlaceholder()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.Placeholder, "Enter your message here..."));
|
||||
Assert.Contains("Enter your message here...", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldRenderWithCustomEmptyMessage()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.EmptyMessage, "No messages yet"));
|
||||
Assert.Contains("No messages yet", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldShowClearButtonByDefault()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
Assert.Contains("clear_all", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldHideClearButtonWhenShowClearButtonIsFalse()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.ShowClearButton, false));
|
||||
Assert.DoesNotContain("clear_all", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldBeDisabledWhenDisabledIsTrue()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.Disabled, true));
|
||||
Assert.Contains("disabled", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldBeReadOnlyWhenReadOnlyIsTrue()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
|
||||
.Add(p => p.ReadOnly, true));
|
||||
Assert.Contains("readonly", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldHaveCorrectCssClass()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
Assert.Contains("rz-chat", component.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChatMessage_ShouldHaveCorrectProperties()
|
||||
{
|
||||
// Arrange
|
||||
var message = new ChatMessage
|
||||
{
|
||||
Content = "Test message",
|
||||
IsUser = true,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
// Assert
|
||||
Assert.NotEmpty(message.Id);
|
||||
Assert.Equal("Test message", message.Content);
|
||||
Assert.True(message.IsUser);
|
||||
Assert.False(message.IsStreaming);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_AddMessage_ShouldAddMessageToList()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
// Act
|
||||
component.Instance.AddMessage("Test message", true);
|
||||
// Assert
|
||||
var messages = component.Instance.GetMessages();
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("Test message", messages[0].Content);
|
||||
Assert.True(messages[0].IsUser);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ClearChat_ShouldRemoveAllMessages()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>();
|
||||
component.Instance.AddMessage("Test message 1", true);
|
||||
component.Instance.AddMessage("Test message 2", false);
|
||||
// Act
|
||||
component.InvokeAsync(async () => await component.Instance.ClearChat()).Wait();
|
||||
// Assert
|
||||
Assert.Empty(component.Instance.GetMessages());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RadzenAIChat_ShouldLimitMessagesToMaxMessages()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
RegisterChatService(ctx);
|
||||
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters.Add(p => p.MaxMessages, 3));
|
||||
component.Instance.AddMessage("Message 1", true);
|
||||
component.Instance.AddMessage("Message 2", false);
|
||||
component.Instance.AddMessage("Message 3", true);
|
||||
component.Instance.AddMessage("Message 4", false);
|
||||
// Assert
|
||||
var messages = component.Instance.GetMessages();
|
||||
Assert.Equal(3, messages.Count);
|
||||
Assert.Equal("Message 2", messages[0].Content);
|
||||
Assert.Equal("Message 3", messages[1].Content);
|
||||
Assert.Equal("Message 4", messages[2].Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
459
Radzen.Blazor/AIChatService.cs
Normal file
459
Radzen.Blazor/AIChatService.cs
Normal file
@@ -0,0 +1,459 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Linq;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chat message in the conversation history.
|
||||
/// </summary>
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the role of the message sender (system, user, or assistant).
|
||||
/// </summary>
|
||||
public string Role { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content of the message.
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the message was created.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a conversation session with memory.
|
||||
/// </summary>
|
||||
public class ConversationSession
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the conversation session.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of messages in the conversation.
|
||||
/// </summary>
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the conversation was created.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the conversation was last updated.
|
||||
/// </summary>
|
||||
public DateTime LastUpdated { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in memory.
|
||||
/// </summary>
|
||||
public int MaxMessages { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to the conversation and manages memory limits.
|
||||
/// </summary>
|
||||
/// <param name="role">The role of the message sender.</param>
|
||||
/// <param name="content">The message content.</param>
|
||||
public void AddMessage(string role, string content)
|
||||
{
|
||||
Messages.Add(new ChatMessage
|
||||
{
|
||||
Role = role,
|
||||
Content = content,
|
||||
Timestamp = DateTime.Now
|
||||
});
|
||||
|
||||
LastUpdated = DateTime.Now;
|
||||
|
||||
// Remove oldest messages if we exceed the limit
|
||||
while (Messages.Count > MaxMessages)
|
||||
{
|
||||
Messages.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all messages from the conversation.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Messages.Clear();
|
||||
LastUpdated = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conversation messages formatted for the AI API.
|
||||
/// </summary>
|
||||
/// <param name="systemPrompt">The system prompt to include.</param>
|
||||
/// <returns>A list of message objects for the AI API.</returns>
|
||||
public List<object> GetFormattedMessages(string systemPrompt)
|
||||
{
|
||||
var messages = new List<object>();
|
||||
|
||||
// Add system message
|
||||
messages.Add(new { role = "system", content = systemPrompt });
|
||||
|
||||
// Add conversation messages
|
||||
foreach (var message in Messages)
|
||||
{
|
||||
messages.Add(new { role = message.Role, content = message.Content });
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for getting chat completions from an AI model with conversation memory.
|
||||
/// </summary>
|
||||
public interface IAIChatService
|
||||
{
|
||||
/// <summary>
|
||||
/// Streams chat completion responses from the AI model asynchronously with conversation memory.
|
||||
/// </summary>
|
||||
/// <param name="userInput">The user's input message to send to the AI model.</param>
|
||||
/// <param name="sessionId">Optional session ID to maintain conversation context. If null, a new session will be created.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <param name="model">Optional model name to override the configured model.</param>
|
||||
/// <param name="systemPrompt">Optional system prompt to override the configured system prompt.</param>
|
||||
/// <param name="temperature">Optional temperature to override the configured temperature.</param>
|
||||
/// <param name="maxTokens">Optional maximum tokens to override the configured max tokens.</param>
|
||||
/// <returns>An async enumerable that yields streaming response chunks from the AI model.</returns>
|
||||
IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string? sessionId = null, CancellationToken cancellationToken = default, string? model = null, string? systemPrompt = null, double? temperature = null, int? maxTokens = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates a conversation session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session ID. If null, a new session will be created.</param>
|
||||
/// <returns>The conversation session.</returns>
|
||||
ConversationSession GetOrCreateSession(string? sessionId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the conversation history for a specific session.
|
||||
/// </summary>
|
||||
/// <param name="sessionId">The session ID to clear.</param>
|
||||
void ClearSession(string sessionId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all active conversation sessions.
|
||||
/// </summary>
|
||||
/// <returns>A list of active conversation sessions.</returns>
|
||||
IEnumerable<ConversationSession> GetActiveSessions();
|
||||
|
||||
/// <summary>
|
||||
/// Removes old conversation sessions based on age.
|
||||
/// </summary>
|
||||
/// <param name="maxAgeHours">Maximum age in hours for sessions to keep.</param>
|
||||
void CleanupOldSessions(int maxAgeHours = 24);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the <see cref="AIChatService"/>.
|
||||
/// </summary>
|
||||
public class AIChatServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the endpoint URL for the AI service.
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the proxy URL for the AI service, if any. If set, this will override the Endpoint.
|
||||
/// </summary>
|
||||
public string Proxy { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the API key for authentication with the AI service.
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the header name for the API key (e.g., 'Authorization' or 'api-key').
|
||||
/// </summary>
|
||||
public string ApiKeyHeader { get; set; } = "Authorization";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the model name to use for executing chat completions (e.g., 'gpt-3.5-turbo').
|
||||
/// </summary>
|
||||
public string Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the system prompt for the AI assistant.
|
||||
/// </summary>
|
||||
public string SystemPrompt { get; set; } = "You are a helpful AI code assistant.";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the temperature for the AI model (0.0 to 2.0). Set to 0.0 for deterministic responses, higher values for more creative outputs.
|
||||
/// </summary>
|
||||
public double Temperature { get; set; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of tokens to generate in the response.
|
||||
/// </summary>
|
||||
public int? MaxTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in conversation memory.
|
||||
/// </summary>
|
||||
public int MaxMessages { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum age in hours for conversation sessions before cleanup.
|
||||
/// </summary>
|
||||
public int SessionMaxAgeHours { get; set; } = 24;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for interacting with AI chat models to get completions with conversation memory.
|
||||
/// </summary>
|
||||
public class AIChatService(HttpClient httpClient, IOptions<AIChatServiceOptions> options) : IAIChatService
|
||||
{
|
||||
private readonly Dictionary<string, ConversationSession> _sessions = new();
|
||||
private readonly object _sessionsLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration options for the chat streaming service.
|
||||
/// </summary>
|
||||
public AIChatServiceOptions Options => options.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string? sessionId = null, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default, string? model = null, string? systemPrompt = null, double? temperature = null, int? maxTokens = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput))
|
||||
{
|
||||
throw new ArgumentException("User input cannot be null or empty.", nameof(userInput));
|
||||
}
|
||||
|
||||
// Get or create session
|
||||
var session = GetOrCreateSession(sessionId);
|
||||
|
||||
// Add user message to conversation history
|
||||
session.AddMessage("user", userInput);
|
||||
|
||||
var url = Options.Proxy ?? Options.Endpoint;
|
||||
|
||||
// Get formatted messages including conversation history
|
||||
var messages = session.GetFormattedMessages(systemPrompt ?? Options.SystemPrompt);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model = model ?? Options.Model,
|
||||
messages = messages,
|
||||
temperature = temperature ?? Options.Temperature,
|
||||
max_tokens = maxTokens ?? Options.MaxTokens,
|
||||
stream = true
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), Encoding.UTF8, "application/json")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(Options.ApiKey))
|
||||
{
|
||||
if (string.Equals(Options.ApiKeyHeader, "Authorization", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Options.ApiKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Add(Options.ApiKeyHeader, Options.ApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new Exception($"Chat stream failed: {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
||||
}
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var assistantResponse = new StringBuilder();
|
||||
|
||||
while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var line = await reader.ReadLineAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data:"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var json = line["data:".Length..].Trim();
|
||||
|
||||
if (json == "[DONE]")
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var content = ParseStreamingResponse(json);
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
assistantResponse.Append(content);
|
||||
yield return content;
|
||||
}
|
||||
}
|
||||
|
||||
// Add assistant response to conversation history
|
||||
if (assistantResponse.Length > 0)
|
||||
{
|
||||
session.AddMessage("assistant", assistantResponse.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConversationSession GetOrCreateSession(string? sessionId = null)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
sessionId = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
if (!_sessions.TryGetValue(sessionId, out var session))
|
||||
{
|
||||
session = new ConversationSession
|
||||
{
|
||||
Id = sessionId,
|
||||
MaxMessages = Options.MaxMessages
|
||||
};
|
||||
_sessions[sessionId] = session;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearSession(string sessionId)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
if (_sessions.TryGetValue(sessionId, out var session))
|
||||
{
|
||||
session.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ConversationSession> GetActiveSessions()
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
return _sessions.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CleanupOldSessions(int maxAgeHours = 24)
|
||||
{
|
||||
lock (_sessionsLock)
|
||||
{
|
||||
var cutoffTime = DateTime.Now.AddHours(-maxAgeHours);
|
||||
var sessionsToRemove = _sessions.Values
|
||||
.Where(s => s.LastUpdated < cutoffTime)
|
||||
.Select(s => s.Id)
|
||||
.ToList();
|
||||
|
||||
foreach (var sessionId in sessionsToRemove)
|
||||
{
|
||||
_sessions.Remove(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ParseStreamingResponse(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("choices", out var choices) || choices.GetArrayLength() == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var firstChoice = choices[0];
|
||||
|
||||
if (!firstChoice.TryGetProperty("delta", out var delta))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (delta.TryGetProperty("content", out var contentElement))
|
||||
{
|
||||
return contentElement.GetString() ?? string.Empty;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring AIChatService in the dependency injection container.
|
||||
/// </summary>
|
||||
public static class AIChatServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the AIChatService to the service collection with the specified configuration.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configureOptions">The action to configure the AIChatService options.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
public static IServiceCollection AddAIChatService(this IServiceCollection services, Action<AIChatServiceOptions> configureOptions)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (configureOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configureOptions));
|
||||
}
|
||||
|
||||
services.Configure(configureOptions);
|
||||
services.AddScoped<IAIChatService, AIChatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the AIChatService to the service collection with default options.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The updated service collection.</returns>
|
||||
public static IServiceCollection AddAIChatService(this IServiceCollection services)
|
||||
{
|
||||
services.AddOptions<AIChatServiceOptions>();
|
||||
services.AddScoped<IAIChatService, AIChatService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
100
Radzen.Blazor/RadzenAIChat.razor
Normal file
100
Radzen.Blazor/RadzenAIChat.razor
Normal file
@@ -0,0 +1,100 @@
|
||||
@using Radzen
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using System.Net.Http
|
||||
|
||||
@inherits RadzenComponent
|
||||
|
||||
@inject IAIChatService ChatService
|
||||
@inject HttpClient Http
|
||||
|
||||
@if (Visible)
|
||||
{
|
||||
<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" style="@Style" id="@GetId()" >
|
||||
<!-- Chat Header -->
|
||||
@if (!string.IsNullOrWhiteSpace(Title) || ShowClearButton)
|
||||
{
|
||||
<RadzenStack class="rz-chat-header" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.SpaceBetween" Gap="0.5rem">
|
||||
@if (!string.IsNullOrWhiteSpace(Title))
|
||||
{
|
||||
<span class="rz-chat-header-title">@Title</span>
|
||||
}
|
||||
<RadzenStack class="rz-chat-header-actions" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Gap="0.5rem">
|
||||
@if (ShowClearButton)
|
||||
{
|
||||
<RadzenButton Icon="delete_history" Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
|
||||
Click="@OnClearChat" Title="Clear chat history" class="rz-chat-header-clear" />
|
||||
}
|
||||
</RadzenStack>
|
||||
</RadzenStack>
|
||||
}
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<div class="rz-chat-messages" @ref="messagesContainer">
|
||||
@if (Messages.Count == 0)
|
||||
{
|
||||
<RadzenStack class="rz-chat-empty" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Gap="1rem">
|
||||
<RadzenIcon Icon="chat_bubble_outline" class="rz-text-secondary-color" />
|
||||
<RadzenText TextStyle="TextStyle.Body2" class="rz-mt-2 rz-text-tertiary-color">@EmptyMessage</RadzenText>
|
||||
</RadzenStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
<div class="rz-chat-message @(message.IsUser ? "rz-chat-message-user" : "rz-chat-message-assistant")" @key="message.Id">
|
||||
<RadzenStack class="rz-chat-message-avatar" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Gap="0">
|
||||
@if (message.IsUser)
|
||||
{
|
||||
<span class="rz-chat-avatar-initials" title="@UserAvatarText">@UserAvatarText</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="rz-chat-avatar-initials" title="@AssistantAvatarText">@AssistantAvatarText</span>
|
||||
}
|
||||
</RadzenStack>
|
||||
<RadzenStack Orientation="Orientation.Vertical" AlignItems="@(message.IsUser ? AlignItems.End : AlignItems.Start)" Gap="0.125rem" class="rz-chat-message-content">
|
||||
<div class="rz-chat-message-text">
|
||||
@if (message.IsStreaming)
|
||||
{
|
||||
@message.Content <RadzenIcon Icon="more_horiz" class="rz-chat-message-streaming-icon" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<RadzenMarkdown Text=@message.Content />
|
||||
}
|
||||
</div>
|
||||
<div class="rz-chat-message-time">
|
||||
@message.Timestamp.ToString("HH:mm")
|
||||
</div>
|
||||
</RadzenStack>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Chat Input -->
|
||||
<div class="rz-chat-input">
|
||||
<div class="rz-chat-input-inner">
|
||||
<textarea @ref="inputElement"
|
||||
value="@CurrentInput"
|
||||
@oninput="OnInput"
|
||||
@onkeydown="OnKeyDown"
|
||||
@onkeydown:preventDefault="preventDefault"
|
||||
class="rz-textarea rz-chat-textarea"
|
||||
placeholder="@Placeholder"
|
||||
disabled="@Disabled"
|
||||
readonly="@ReadOnly"
|
||||
rows="1"
|
||||
@attributes="@InputAttributes"></textarea>
|
||||
<RadzenButton Icon="send"
|
||||
ButtonStyle="ButtonStyle.Primary"
|
||||
Variant="Variant.Text"
|
||||
Disabled="@(string.IsNullOrWhiteSpace(CurrentInput) || IsLoading || Disabled)"
|
||||
Click="@OnSendMessage"
|
||||
Title="Send message"
|
||||
class="rz-chat-send-btn" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
460
Radzen.Blazor/RadzenAIChat.razor.cs
Normal file
460
Radzen.Blazor/RadzenAIChat.razor.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a chat message in the RadzenAIChat component.
|
||||
/// </summary>
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the message.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content of the message.
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this message is from the user.
|
||||
/// </summary>
|
||||
public bool IsUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the message was created.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this message is currently streaming.
|
||||
/// </summary>
|
||||
public bool IsStreaming { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RadzenAIChat component that provides a modern chat interface with AI integration and conversation memory.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// <RadzenAIChat Title="AI Assistant" Placeholder="Type your message..." @bind-Messages="@chatMessages" SessionId="@sessionId" />
|
||||
/// </code>
|
||||
/// </example>
|
||||
public partial class RadzenAIChat : RadzenComponent
|
||||
{
|
||||
private List<ChatMessage> Messages { get; set; } = new();
|
||||
private string CurrentInput { get; set; } = string.Empty;
|
||||
private bool IsLoading { get; set; } = false;
|
||||
private bool preventDefault = false;
|
||||
private ElementReference inputElement;
|
||||
private ElementReference messagesContainer;
|
||||
private CancellationTokenSource cts = new();
|
||||
private string? currentSessionId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the session ID for maintaining conversation memory. If null, a new session will be created.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? SessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event callback that is invoked when a session ID is created or retrieved.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> SessionIdChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies additional custom attributes that will be rendered by the input.
|
||||
/// </summary>
|
||||
/// <value>The attributes.</value>
|
||||
[Parameter]
|
||||
public IReadOnlyDictionary<string, object> InputAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title displayed in the chat header.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the placeholder text for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; } = "Type your message...";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the message displayed when there are no messages.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string EmptyMessage { get; set; } = "No messages yet. Start a conversation!";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text displayed in the user avatar.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string UserAvatarText { get; set; } = "U";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text displayed in the assistant avatar.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string AssistantAvatarText { get; set; } = "AI";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to show the clear chat button.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowClearButton { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the chat is disabled.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the input is read-only.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of messages to keep in the chat.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int MaxMessages { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Event callback that is invoked when a new message is added.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<ChatMessage> MessageAdded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event callback that is invoked when the chat is cleared.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback ChatCleared { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event callback that is invoked when a message is sent.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> MessageSent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event callback that is invoked when the AI response is received.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> ResponseReceived { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current list of messages.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ChatMessage> GetMessages() => Messages.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current session ID.
|
||||
/// </summary>
|
||||
public string? GetSessionId() => currentSessionId;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to the chat.
|
||||
/// </summary>
|
||||
/// <param name="content">The message content.</param>
|
||||
/// <param name="isUser">Whether the message is from the user.</param>
|
||||
/// <returns>The created message.</returns>
|
||||
public ChatMessage AddMessage(string content, bool isUser = false)
|
||||
{
|
||||
var message = new ChatMessage
|
||||
{
|
||||
Content = content,
|
||||
IsUser = isUser,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
|
||||
Messages.Add(message);
|
||||
|
||||
// Limit the number of messages
|
||||
if (Messages.Count > MaxMessages)
|
||||
{
|
||||
Messages.RemoveAt(0);
|
||||
}
|
||||
|
||||
InvokeAsync(StateHasChanged);
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all messages from the chat.
|
||||
/// </summary>
|
||||
public async Task ClearChat()
|
||||
{
|
||||
Messages.Clear();
|
||||
|
||||
// Clear the session in the AI service
|
||||
if (!string.IsNullOrEmpty(currentSessionId))
|
||||
{
|
||||
ChatService.ClearSession(currentSessionId);
|
||||
}
|
||||
|
||||
await ChatCleared.InvokeAsync();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message programmatically.
|
||||
/// </summary>
|
||||
/// <param name="content">The message content to send.</param>
|
||||
public async Task SendMessage(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content) || Disabled || IsLoading)
|
||||
return;
|
||||
|
||||
// Add user message
|
||||
var userMessage = AddMessage(content, true);
|
||||
await MessageAdded.InvokeAsync(userMessage);
|
||||
await MessageSent.InvokeAsync(content);
|
||||
|
||||
// Clear input
|
||||
CurrentInput = string.Empty;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// Get AI response
|
||||
await GetAIResponse(content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message programmatically with custom AI parameters.
|
||||
/// </summary>
|
||||
/// <param name="content">The message content to send.</param>
|
||||
/// <param name="model">Optional model name to override the configured model.</param>
|
||||
/// <param name="systemPrompt">Optional system prompt to override the configured system prompt.</param>
|
||||
/// <param name="temperature">Optional temperature to override the configured temperature.</param>
|
||||
/// <param name="maxTokens">Optional maximum tokens to override the configured max tokens.</param>
|
||||
public async Task SendMessage(string content, string? model = null, string? systemPrompt = null, double? temperature = null, int? maxTokens = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content) || Disabled || IsLoading)
|
||||
return;
|
||||
|
||||
// Add user message
|
||||
var userMessage = AddMessage(content, true);
|
||||
await MessageAdded.InvokeAsync(userMessage);
|
||||
await MessageSent.InvokeAsync(content);
|
||||
|
||||
// Clear input
|
||||
CurrentInput = string.Empty;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// Get AI response with custom parameters
|
||||
await GetAIResponse(content, model, systemPrompt, temperature, maxTokens);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads conversation history from the AI service session.
|
||||
/// </summary>
|
||||
public async Task LoadConversationHistory()
|
||||
{
|
||||
if (string.IsNullOrEmpty(currentSessionId))
|
||||
return;
|
||||
|
||||
var session = ChatService.GetOrCreateSession(currentSessionId);
|
||||
|
||||
// Clear current messages
|
||||
Messages.Clear();
|
||||
|
||||
// Add messages from session history
|
||||
foreach (var message in session.Messages)
|
||||
{
|
||||
var isUser = message.Role.Equals("user", StringComparison.OrdinalIgnoreCase);
|
||||
AddMessage(message.Content, isUser);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task GetAIResponse(string userInput)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput))
|
||||
return;
|
||||
|
||||
IsLoading = true;
|
||||
cts.Cancel();
|
||||
cts = new CancellationTokenSource();
|
||||
|
||||
// Ensure we have a session ID
|
||||
if (string.IsNullOrEmpty(currentSessionId))
|
||||
{
|
||||
currentSessionId = SessionId ?? Guid.NewGuid().ToString();
|
||||
await SessionIdChanged.InvokeAsync(currentSessionId);
|
||||
}
|
||||
|
||||
// Add assistant message placeholder
|
||||
var assistantMessage = AddMessage("", false);
|
||||
assistantMessage.IsStreaming = true;
|
||||
|
||||
try
|
||||
{
|
||||
var response = "";
|
||||
await foreach (var token in ChatService.GetCompletionsAsync(userInput, currentSessionId, cts.Token))
|
||||
{
|
||||
response += token;
|
||||
assistantMessage.Content = response;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
assistantMessage.IsStreaming = false;
|
||||
await ResponseReceived.InvokeAsync(response);
|
||||
await MessageAdded.InvokeAsync(assistantMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
assistantMessage.Content = $"Sorry, I encountered an error: {ex.Message}";
|
||||
assistantMessage.IsStreaming = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GetAIResponse(string userInput, string? model = null, string? systemPrompt = null, double? temperature = null, int? maxTokens = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput))
|
||||
return;
|
||||
|
||||
IsLoading = true;
|
||||
cts.Cancel();
|
||||
cts = new CancellationTokenSource();
|
||||
|
||||
// Ensure we have a session ID
|
||||
if (string.IsNullOrEmpty(currentSessionId))
|
||||
{
|
||||
currentSessionId = SessionId ?? Guid.NewGuid().ToString();
|
||||
await SessionIdChanged.InvokeAsync(currentSessionId);
|
||||
}
|
||||
|
||||
// Add assistant message placeholder
|
||||
var assistantMessage = AddMessage("", false);
|
||||
assistantMessage.IsStreaming = true;
|
||||
|
||||
try
|
||||
{
|
||||
var response = "";
|
||||
await foreach (var token in ChatService.GetCompletionsAsync(userInput, currentSessionId, cts.Token, model, systemPrompt, temperature, maxTokens))
|
||||
{
|
||||
response += token;
|
||||
assistantMessage.Content = response;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
assistantMessage.IsStreaming = false;
|
||||
await ResponseReceived.InvokeAsync(response);
|
||||
await MessageAdded.InvokeAsync(assistantMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
assistantMessage.Content = $"Sorry, I encountered an error: {ex.Message}";
|
||||
assistantMessage.IsStreaming = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Initialize session ID
|
||||
currentSessionId = SessionId ?? Guid.NewGuid().ToString();
|
||||
if (currentSessionId != SessionId)
|
||||
{
|
||||
await SessionIdChanged.InvokeAsync(currentSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
|
||||
// Update session ID if it changed
|
||||
if (SessionId != currentSessionId)
|
||||
{
|
||||
currentSessionId = SessionId ?? Guid.NewGuid().ToString();
|
||||
await SessionIdChanged.InvokeAsync(currentSessionId);
|
||||
|
||||
// Load conversation history for the new session
|
||||
await LoadConversationHistory();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnInput(ChangeEventArgs e)
|
||||
{
|
||||
CurrentInput = e.Value?.ToString() ?? "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter" && !e.ShiftKey)
|
||||
{
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", inputElement, "");
|
||||
preventDefault = true;
|
||||
await OnSendMessage();
|
||||
}
|
||||
else
|
||||
{
|
||||
preventDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSendMessage()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(CurrentInput))
|
||||
{
|
||||
await SendMessage(CurrentInput);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnClearChat()
|
||||
{
|
||||
await ClearChat();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender && messagesContainer.Context != null)
|
||||
{
|
||||
// Scroll to bottom when new messages are added
|
||||
await JSRuntime.InvokeVoidAsync("eval",
|
||||
"setTimeout(() => { " +
|
||||
"const container = document.querySelector('.rz-chat-messages'); " +
|
||||
"if (container) container.scrollTop = container.scrollHeight; " +
|
||||
"}, 100);");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetComponentCssClass()
|
||||
{
|
||||
return ClassList.Create("rz-chat").ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,4 +80,5 @@
|
||||
@import 'components/blazor/timespanpicker';
|
||||
@import 'components/blazor/toc';
|
||||
@import 'components/blazor/expander';
|
||||
@import 'components/blazor/chat';
|
||||
@import 'css-variables';
|
||||
|
||||
@@ -193,6 +193,30 @@
|
||||
--rz-chart-tooltip-item-hover-background-color: #{$chart-tooltip-item-hover-background-color};
|
||||
--rz-chart-marker-stroke: #{$chart-marker-stroke};
|
||||
|
||||
/* Chat */
|
||||
--rz-chat-background-color: #{$rz-chat-background-color};
|
||||
--rz-chat-border: #{$rz-chat-border};
|
||||
--rz-chat-border-radius: #{$rz-chat-border-radius};
|
||||
--rz-chat-header-padding-block: #{$rz-chat-header-padding-block};
|
||||
--rz-chat-header-padding-inline: #{$rz-chat-header-padding-inline};
|
||||
--rz-chat-header-background-color: #{$rz-chat-header-background-color};
|
||||
--rz-chat-header-bottom-border: #{$rz-chat-header-bottom-border};
|
||||
--rz-chat-header-shadow: #{$rz-chat-header-shadow};
|
||||
--rz-chat-header-title-font-size: #{$rz-chat-header-title-font-size};
|
||||
--rz-chat-header-title-font-weight: #{$rz-chat-header-title-font-weight};
|
||||
--rz-chat-message-user-color: #{$rz-chat-message-user-color};
|
||||
--rz-chat-message-user-background-color: #{$rz-chat-message-user-background-color};
|
||||
--rz-chat-message-assistant-color: #{$rz-chat-message-assistant-color};
|
||||
--rz-chat-message-assistant-background-color: #{$rz-chat-message-assistant-background-color};
|
||||
--rz-chat-message-text-padding: #{$rz-chat-message-text-padding};
|
||||
--rz-chat-message-text-border-radius: #{$rz-chat-message-text-border-radius};
|
||||
--rz-chat-avatar-size: #{$rz-chat-avatar-size};
|
||||
--rz-chat-avatar-border-radius: #{$rz-chat-avatar-border-radius};
|
||||
--rz-chat-avatar-font-size: #{$rz-chat-avatar-font-size};
|
||||
--rz-chat-avatar-font-weight: #{$rz-chat-avatar-font-weight};
|
||||
--rz-chat-message-time-padding: #{$rz-chat-message-time-padding};
|
||||
--rz-chat-message-callout-radius: #{$rz-chat-message-callout-radius};
|
||||
|
||||
/* Checkbox */
|
||||
--rz-checkbox-width: #{$checkbox-width};
|
||||
--rz-checkbox-height: #{$checkbox-height};
|
||||
|
||||
@@ -31,4 +31,8 @@
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes rz-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
196
Radzen.Blazor/themes/components/blazor/_chat.scss
Normal file
196
Radzen.Blazor/themes/components/blazor/_chat.scss
Normal file
@@ -0,0 +1,196 @@
|
||||
$rz-chat-background-color: var(--rz-base-background-color) !default;
|
||||
$rz-chat-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-header-padding-block: 0.5rem !default;
|
||||
$rz-chat-header-padding-inline: 1rem 0.5rem !default;
|
||||
$rz-chat-header-background-color: $rz-chat-background-color !default;
|
||||
$rz-chat-header-bottom-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-header-shadow: none !default;
|
||||
$rz-chat-header-title-font-size: 1rem !default;
|
||||
$rz-chat-header-title-font-weight: 600 !default;
|
||||
$rz-chat-message-user-color: var(--rz-on-primary-lighter) !default;
|
||||
$rz-chat-message-user-background-color: var(--rz-primary-lighter) !default;
|
||||
$rz-chat-message-assistant-color: var(--rz-on-base) !default;
|
||||
$rz-chat-message-assistant-background-color: var(--rz-base) !default;
|
||||
$rz-chat-message-text-padding: 0.25rem 0.5rem !default;
|
||||
$rz-chat-message-text-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-size: 2rem !default;
|
||||
$rz-chat-avatar-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-font-size: 1rem !default;
|
||||
$rz-chat-avatar-font-weight: 700 !default;
|
||||
$rz-chat-message-time-padding: 0 !default;
|
||||
$rz-chat-message-callout-radius: calc(var(--rz-border-radius) / 4) !default;
|
||||
|
||||
// Chat Component Styles - ChatGPT-like UI
|
||||
.rz-chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--rz-chat-background-color);
|
||||
border: var(--rz-chat-border);
|
||||
border-radius: var(--rz-chat-border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rz-chat-header {
|
||||
z-index: 1;
|
||||
padding-block: var(--rz-chat-header-padding-block);
|
||||
padding-inline: var(--rz-chat-header-padding-inline);
|
||||
background-color: var(--rz-chat-header-background-color);
|
||||
border-bottom: var(--rz-chat-header-bottom-border);
|
||||
box-shadow: var(--rz-chat-header-shadow);
|
||||
}
|
||||
|
||||
.rz-chat-header-title {
|
||||
flex: 1;
|
||||
font-size: var(--rz-chat-header-title-font-size);
|
||||
font-weight: var(--rz-chat-header-title-font-weight);
|
||||
}
|
||||
|
||||
.rz-chat-messages {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.rz-chat-empty {
|
||||
height: 100%;
|
||||
|
||||
.rzi {
|
||||
font-size: 3rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.rz-chat-message {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.5rem;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.rz-chat-message-text {
|
||||
padding: var(--rz-chat-message-text-padding);
|
||||
border-radius: var(--rz-chat-message-text-border-radius);
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
--rz-text-h1-margin-block-end: 0;
|
||||
--rz-text-h2-margin-block-end: 0;
|
||||
--rz-text-h3-margin-block-end: 0;
|
||||
--rz-text-h4-margin-block-end: 0;
|
||||
--rz-text-h5-margin-block-end: 0;
|
||||
--rz-text-h6-margin-block-end: 0;
|
||||
--rz-text-body1-margin-block-end: 0;
|
||||
--rz-text-body2-margin-block-end: 0;
|
||||
}
|
||||
|
||||
.rz-chat-message-avatar {
|
||||
width: var(--rz-chat-avatar-size);
|
||||
height: var(--rz-chat-avatar-size);
|
||||
border-radius: var(--rz-chat-avatar-border-radius);
|
||||
font-size: var(--rz-chat-avatar-font-size);
|
||||
font-weight: var(--rz-chat-avatar-font-weight);
|
||||
background-color: var(--rz-base);
|
||||
color: var(--rz-on-base);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rz-chat-avatar-initials {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rz-chat-message-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--rz-text-disabled-color);
|
||||
padding: var(--rz-chat-message-time-padding);
|
||||
}
|
||||
|
||||
.rz-chat-message-user {
|
||||
align-self: flex-end;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.rz-chat-message-avatar {
|
||||
background-color: var(--rz-chat-message-user-background-color);
|
||||
color: var(--rz-chat-message-user-color);
|
||||
}
|
||||
|
||||
.rz-chat-message-text {
|
||||
background-color: var(--rz-chat-message-user-background-color);
|
||||
color: var(--rz-chat-message-user-color);
|
||||
border-end-end-radius: var(--rz-chat-message-callout-radius);
|
||||
/* --rz-text-h1-color: var(--rz-chat-message-user-color);
|
||||
--rz-text-h2-color: var(--rz-chat-message-user-color);
|
||||
--rz-text-h3-color: var(--rz-chat-message-user-color);
|
||||
--rz-text-h4-color: var(--rz-chat-message-user-color);
|
||||
--rz-text-h5-color: var(--rz-chat-message-user-color);
|
||||
--rz-text-h6-color: var(--rz-chat-message-user-color);
|
||||
--rz-text-body1-color: var(--rz-chat-message-user-color);
|
||||
--rz-text-body2-color: var(--rz-chat-message-user-color); */
|
||||
}
|
||||
}
|
||||
|
||||
.rz-chat-message-assistant {
|
||||
align-self: flex-start;
|
||||
|
||||
.rz-chat-message-avatar {
|
||||
background-color: var(--rz-chat-message-assistant-background-color);
|
||||
color: var(--rz-chat-message-assistant-color);
|
||||
}
|
||||
|
||||
.rz-chat-message-text {
|
||||
background-color: var(--rz-chat-message-assistant-background-color);
|
||||
color: var(--rz-chat-message-assistant-color);
|
||||
border-end-start-radius: var(--rz-chat-message-callout-radius);
|
||||
/* --rz-text-h1-color: var(--rz-chat-message-assistant-color);
|
||||
--rz-text-h2-color: var(--rz-chat-message-assistant-color);
|
||||
--rz-text-h3-color: var(--rz-chat-message-assistant-color);
|
||||
--rz-text-h4-color: var(--rz-chat-message-assistant-color);
|
||||
--rz-text-h5-color: var(--rz-chat-message-assistant-color);
|
||||
--rz-text-h6-color: var(--rz-chat-message-assistant-color);
|
||||
--rz-text-body1-color: var(--rz-chat-message-assistant-color);
|
||||
--rz-text-body2-color: var(--rz-chat-message-assistant-color); */
|
||||
}
|
||||
}
|
||||
|
||||
.rz-chat-input {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.rz-chat-input-inner {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.rz-chat-textarea {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
max-height: 10rem;
|
||||
overflow-y: scroll;
|
||||
padding-block: var(--rz-text-area-padding-block);
|
||||
padding-inline: var(--rz-text-area-padding-inline);
|
||||
field-sizing: content;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.rz-chat-message-streaming-icon {
|
||||
animation: rz-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Right-to-left send button */
|
||||
*[dir="rtl"] .rz-chat-send-btn {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
@@ -563,6 +563,30 @@ $rz-toc-link-selected-color: var(--rz-primary) !default;
|
||||
$rz-toc-link-selected-indicator-color: var(--rz-primary) !default;
|
||||
$rz-toc-horizontal-link-selected-color: var(--rz-primary) !default;
|
||||
|
||||
// Chat
|
||||
$rz-chat-background-color: var(--rz-base-background-color) !default;
|
||||
$rz-chat-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-header-padding-block: 0.5rem !default;
|
||||
$rz-chat-header-padding-inline: 1rem 0.5rem !default;
|
||||
$rz-chat-header-background-color: $rz-chat-background-color !default;
|
||||
$rz-chat-header-bottom-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-header-shadow: none !default;
|
||||
$rz-chat-header-title-font-size: 1rem !default;
|
||||
$rz-chat-header-title-font-weight: 600 !default;
|
||||
$rz-chat-message-user-color: var(--rz-on-primary-lighter) !default;
|
||||
$rz-chat-message-user-background-color: var(--rz-primary-lighter) !default;
|
||||
$rz-chat-message-assistant-color: var(--rz-on-base-light) !default;
|
||||
$rz-chat-message-assistant-background-color: var(--rz-base-light) !default;
|
||||
$rz-chat-message-text-padding: 0.5rem 0.75rem !default;
|
||||
$rz-chat-message-text-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-size: 2rem !default;
|
||||
$rz-chat-avatar-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-font-size: 1rem !default;
|
||||
$rz-chat-avatar-font-weight: 700 !default;
|
||||
$rz-chat-message-time-padding: 0 !default;
|
||||
$rz-chat-message-callout-radius: calc(var(--rz-border-radius) / 4) !default;
|
||||
|
||||
@import 'fonts';
|
||||
@import 'components';
|
||||
|
||||
|
||||
@@ -774,6 +774,30 @@ $rz-toc-link-selected-color: var(--rz-primary-light) !default;
|
||||
$rz-toc-link-selected-indicator-color: var(--rz-primary-light) !default;
|
||||
$rz-toc-horizontal-link-selected-color: var(--rz-primary-light) !default;
|
||||
|
||||
// Chat
|
||||
$rz-chat-background-color: var(--rz-base-background-color) !default;
|
||||
$rz-chat-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-header-padding-block: 0.5rem !default;
|
||||
$rz-chat-header-padding-inline: 1rem 0.5rem !default;
|
||||
$rz-chat-header-background-color: $rz-chat-background-color !default;
|
||||
$rz-chat-header-bottom-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-header-shadow: none !default;
|
||||
$rz-chat-header-title-font-size: 1rem !default;
|
||||
$rz-chat-header-title-font-weight: 600 !default;
|
||||
$rz-chat-message-user-color: var(--rz-on-primary-lighter) !default;
|
||||
$rz-chat-message-user-background-color: var(--rz-primary-lighter) !default;
|
||||
$rz-chat-message-assistant-color: var(--rz-on-base-dark) !default;
|
||||
$rz-chat-message-assistant-background-color: var(--rz-base-dark) !default;
|
||||
$rz-chat-message-text-padding: 0.5rem 0.75rem !default;
|
||||
$rz-chat-message-text-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-size: 2rem !default;
|
||||
$rz-chat-avatar-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-font-size: 1rem !default;
|
||||
$rz-chat-avatar-font-weight: 700 !default;
|
||||
$rz-chat-message-time-padding: 0 !default;
|
||||
$rz-chat-message-callout-radius: calc(var(--rz-border-radius) / 4) !default;
|
||||
|
||||
@import 'fonts';
|
||||
@import 'components';
|
||||
|
||||
|
||||
@@ -773,6 +773,30 @@ $rz-toc-link-selected-color: var(--rz-primary-light) !default;
|
||||
$rz-toc-link-selected-indicator-color: var(--rz-primary-light) !default;
|
||||
$rz-toc-horizontal-link-selected-color: var(--rz-primary-light) !default;
|
||||
|
||||
// Chat
|
||||
$rz-chat-background-color: var(--rz-base-background-color) !default;
|
||||
$rz-chat-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-header-padding-block: 0.5rem !default;
|
||||
$rz-chat-header-padding-inline: 1rem 0.5rem !default;
|
||||
$rz-chat-header-background-color: $rz-chat-background-color !default;
|
||||
$rz-chat-header-bottom-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-header-shadow: none !default;
|
||||
$rz-chat-header-title-font-size: 1rem !default;
|
||||
$rz-chat-header-title-font-weight: 600 !default;
|
||||
$rz-chat-message-user-color: var(--rz-on-primary-lighter) !default;
|
||||
$rz-chat-message-user-background-color: var(--rz-primary-lighter) !default;
|
||||
$rz-chat-message-assistant-color: var(--rz-on-base-dark) !default;
|
||||
$rz-chat-message-assistant-background-color: var(--rz-base-dark) !default;
|
||||
$rz-chat-message-text-padding: 0.5rem 0.75rem !default;
|
||||
$rz-chat-message-text-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-size: 2rem !default;
|
||||
$rz-chat-avatar-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-font-size: 1rem !default;
|
||||
$rz-chat-avatar-font-weight: 700 !default;
|
||||
$rz-chat-message-time-padding: 0 !default;
|
||||
$rz-chat-message-callout-radius: calc(var(--rz-border-radius) / 4) !default;
|
||||
|
||||
@import 'fonts';
|
||||
@import 'components';
|
||||
|
||||
|
||||
@@ -565,6 +565,30 @@ $rz-toc-link-selected-color: var(--rz-primary) !default;
|
||||
$rz-toc-link-selected-indicator-color: var(--rz-primary) !default;
|
||||
$rz-toc-horizontal-link-selected-color: var(--rz-primary) !default;
|
||||
|
||||
// Chat
|
||||
$rz-chat-background-color: var(--rz-base-background-color) !default;
|
||||
$rz-chat-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-header-padding-block: 0.5rem !default;
|
||||
$rz-chat-header-padding-inline: 1rem 0.5rem !default;
|
||||
$rz-chat-header-background-color: $rz-chat-background-color !default;
|
||||
$rz-chat-header-bottom-border: var(--rz-border-normal) !default;
|
||||
$rz-chat-header-shadow: none !default;
|
||||
$rz-chat-header-title-font-size: 1rem !default;
|
||||
$rz-chat-header-title-font-weight: 600 !default;
|
||||
$rz-chat-message-user-color: var(--rz-on-primary-lighter) !default;
|
||||
$rz-chat-message-user-background-color: var(--rz-primary-lighter) !default;
|
||||
$rz-chat-message-assistant-color: var(--rz-on-base-light) !default;
|
||||
$rz-chat-message-assistant-background-color: var(--rz-base-light) !default;
|
||||
$rz-chat-message-text-padding: 0.5rem 0.75rem !default;
|
||||
$rz-chat-message-text-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-size: 2rem !default;
|
||||
$rz-chat-avatar-border-radius: var(--rz-border-radius) !default;
|
||||
$rz-chat-avatar-font-size: 1rem !default;
|
||||
$rz-chat-avatar-font-weight: 700 !default;
|
||||
$rz-chat-message-time-padding: 0 !default;
|
||||
$rz-chat-message-callout-radius: calc(var(--rz-border-radius) / 4) !default;
|
||||
|
||||
@import 'fonts';
|
||||
@import 'components';
|
||||
|
||||
|
||||
81
RadzenBlazorDemos.Host/Controllers/AIChatController.cs
Normal file
81
RadzenBlazorDemos.Host/Controllers/AIChatController.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Radzen;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RadzenBlazorDemos
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ChatController(HttpClient httpClient, IOptions<AIChatServiceOptions> chatStreamingServiceOptions) : ControllerBase
|
||||
{
|
||||
private readonly AIChatServiceOptions options = chatStreamingServiceOptions.Value;
|
||||
|
||||
[HttpPost("completions")]
|
||||
public async Task<IActionResult> Completions()
|
||||
{
|
||||
var request = new HttpRequestMessage
|
||||
{
|
||||
Method = new HttpMethod(Request.Method),
|
||||
RequestUri = new Uri(options.Endpoint),
|
||||
Content = new StreamContent(Request.Body)
|
||||
};
|
||||
|
||||
request.RequestUri = new UriBuilder(request.RequestUri) { Query = Request.QueryString.ToString() }.Uri;
|
||||
|
||||
foreach (var header in Request.Headers)
|
||||
{
|
||||
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
|
||||
}
|
||||
|
||||
// Use configurable API key header
|
||||
|
||||
if (string.Equals(options.ApiKeyHeader, "Authorization", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", options.ApiKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Add(options.ApiKeyHeader, options.ApiKey);
|
||||
}
|
||||
|
||||
request.Headers.Host = request.RequestUri.Host;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await httpClient.SendAsync(request);
|
||||
|
||||
Response.StatusCode = (int)response.StatusCode;
|
||||
|
||||
foreach (var header in response.Headers)
|
||||
{
|
||||
Response.Headers[header.Key] = header.Value.ToArray();
|
||||
}
|
||||
|
||||
foreach (var contentHeader in response.Content.Headers)
|
||||
{
|
||||
Response.Headers[contentHeader.Key] = contentHeader.Value.ToArray();
|
||||
}
|
||||
|
||||
Response.Headers.Remove("transfer-encoding");
|
||||
|
||||
if (!response.IsSuccessStatusCode && response.Content.Headers.ContentLength == 0)
|
||||
{
|
||||
return new StatusCodeResult((int)response.StatusCode);
|
||||
}
|
||||
|
||||
var responseContentStream = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
return new FileStreamResult(responseContentStream, response.Content.Headers.ContentType?.ToString());
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return StatusCode(500, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Radzen;
|
||||
@@ -46,6 +47,9 @@ builder.Services.AddScoped<NorthwindService>();
|
||||
builder.Services.AddScoped<NorthwindODataService>();
|
||||
builder.Services.AddSingleton<GitHubService>();
|
||||
|
||||
builder.Services.AddAIChatService(options =>
|
||||
builder.Configuration.GetSection("AIChatService").Bind(options));
|
||||
|
||||
builder.Services.AddLocalization();
|
||||
|
||||
/* --> Uncomment to enable localization
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ErrorOnDuplicatePublishOutputFiles>False</ErrorOnDuplicatePublishOutputFiles>
|
||||
<UserSecretsId>d4f5f92a-c1c5-47b2-bb94-becc7f09133c</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RadzenBlazorDemos\RadzenBlazorDemos.csproj" />
|
||||
|
||||
@@ -5,5 +5,14 @@
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
},
|
||||
"AIChatService": {
|
||||
"Endpoint": "https://api.cloudflare.com/client/v4/accounts/dac31e6601b57aa9edbead03210a6fd6/ai/v1/chat/completions",
|
||||
"ApiKey": "",
|
||||
"ApiKeyHeader": "Authorization",
|
||||
"Model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
|
||||
"SystemPrompt": "You are a helpful AI code assistant.",
|
||||
"Temperature": 0.7,
|
||||
"MaxTokens": 50
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,14 @@
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"AIChatService": {
|
||||
"Endpoint": "https://api.cloudflare.com/client/v4/accounts/dac31e6601b57aa9edbead03210a6fd6/ai/v1/chat/completions",
|
||||
"ApiKey": "",
|
||||
"ApiKeyHeader": "Authorization",
|
||||
"Model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
|
||||
"SystemPrompt": "You are a helpful AI code assistant.",
|
||||
"Temperature": 0.7,
|
||||
"MaxTokens": 2048
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Radzen;
|
||||
using RadzenBlazorDemos;
|
||||
using RadzenBlazorDemos.Data;
|
||||
@@ -39,6 +40,10 @@ builder.Services.AddScoped<NorthwindService>();
|
||||
builder.Services.AddScoped<NorthwindODataService>();
|
||||
builder.Services.AddSingleton<GitHubService>();
|
||||
|
||||
builder.Services.AddAIChatService(options =>
|
||||
builder.Configuration.GetSection("AIChatService").Bind(options));
|
||||
|
||||
|
||||
builder.Services.AddLocalization();
|
||||
|
||||
/* --> Uncomment to enable localization
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>cf316844-9d34-42f9-a2a7-72a167736c64</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,5 +5,14 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AIChatService": {
|
||||
"Endpoint": "https://api.cloudflare.com/client/v4/accounts/dac31e6601b57aa9edbead03210a6fd6/ai/v1/chat/completions",
|
||||
"ApiKey": "",
|
||||
"ApiKeyHeader": "Authorization",
|
||||
"Model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
|
||||
"SystemPrompt": "You are a helpful AI code assistant.",
|
||||
"Temperature": 0.7,
|
||||
"MaxTokens": 2048
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,14 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"AIChatService": {
|
||||
"Endpoint": "https://api.cloudflare.com/client/v4/accounts/dac31e6601b57aa9edbead03210a6fd6/ai/v1/chat/completions",
|
||||
"ApiKey": "",
|
||||
"ApiKeyHeader": "Authorization",
|
||||
"Model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
|
||||
"SystemPrompt": "You are a helpful AI code assistant.",
|
||||
"Temperature": 0.7,
|
||||
"MaxTokens": 2048
|
||||
}
|
||||
}
|
||||
14
RadzenBlazorDemos/Pages/AIChatCompact.razor
Normal file
14
RadzenBlazorDemos/Pages/AIChatCompact.razor
Normal file
@@ -0,0 +1,14 @@
|
||||
@using Radzen
|
||||
@using Radzen.Blazor
|
||||
|
||||
<RadzenStack class="rz-p-0 rz-p-md-12">
|
||||
<RadzenAIChat @ref="compactChat"
|
||||
Placeholder="Quick message..."
|
||||
ShowClearButton="false"
|
||||
Style="height: 300px; max-width: 100%;"
|
||||
/>
|
||||
</RadzenStack>
|
||||
|
||||
@code {
|
||||
RadzenAIChat compactChat;
|
||||
}
|
||||
70
RadzenBlazorDemos/Pages/AIChatConfig.razor
Normal file
70
RadzenBlazorDemos/Pages/AIChatConfig.razor
Normal file
@@ -0,0 +1,70 @@
|
||||
@using Radzen
|
||||
@using Radzen.Blazor
|
||||
|
||||
<RadzenStack class="rz-p-0 rz-p-md-12">
|
||||
<RadzenCard class="rz-p-4" Variant="Variant.Outlined">
|
||||
<RadzenStack Orientation="Orientation.Vertical" Gap="0.5rem">
|
||||
<RadzenLabel Text="Chat Controls" />
|
||||
<RadzenStack Orientation="Orientation.Horizontal" Wrap="FlexWrap.Wrap" Gap="0.5rem">
|
||||
<RadzenButton Text="Add Welcome"
|
||||
Icon="add"
|
||||
Click="@(() => basicChat?.AddMessage("Hello! How can I help you today?", false))"
|
||||
Variant="Variant.Flat" />
|
||||
|
||||
<RadzenButton Text="Add User Message"
|
||||
Icon="person_add"
|
||||
Click="@(() => basicChat?.AddMessage("This is a test message from the user.", true))"
|
||||
Variant="Variant.Flat" />
|
||||
|
||||
<RadzenButton Text="Clear Chat"
|
||||
Icon="delete_history"
|
||||
ButtonStyle="ButtonStyle.Light"
|
||||
Click="@(() => basicChat?.ClearChat())"
|
||||
Variant="Variant.Flat" />
|
||||
|
||||
<RadzenButton Text="Send Test"
|
||||
Icon="send"
|
||||
ButtonStyle="ButtonStyle.Primary"
|
||||
Click="@(() => basicChat?.SendMessage("What is Blazor?"))"
|
||||
Variant="Variant.Flat" />
|
||||
</RadzenStack>
|
||||
</RadzenStack>
|
||||
</RadzenCard>
|
||||
<RadzenAIChat @ref="basicChat"
|
||||
Title="AI Assistant"
|
||||
Placeholder="Ask me anything..."
|
||||
Style="height: 500px;"
|
||||
MessageAdded="@OnMessageAdded"
|
||||
MessageSent="@OnMessageSent"
|
||||
ResponseReceived="@OnResponseReceived"
|
||||
ChatCleared="@OnChatCleared"
|
||||
/>
|
||||
</RadzenStack>
|
||||
|
||||
<EventConsole @ref="console" Style="min-height: 230px;" />
|
||||
|
||||
@code {
|
||||
RadzenAIChat basicChat;
|
||||
EventConsole console;
|
||||
|
||||
void OnMessageAdded(Radzen.Blazor.ChatMessage message)
|
||||
{
|
||||
console.Log($"Message added: {(message.IsUser ? "User" : "Assistant")} - {message.Content.Substring(0, Math.Min(50, message.Content.Length))}...",
|
||||
message.IsUser ? AlertStyle.Info : AlertStyle.Success);
|
||||
}
|
||||
|
||||
void OnMessageSent(string message)
|
||||
{
|
||||
console.Log($"Message sent: {message}", AlertStyle.Info);
|
||||
}
|
||||
|
||||
void OnResponseReceived(string response)
|
||||
{
|
||||
console.Log($"AI Response received: {response.Substring(0, Math.Min(50, response.Length))}...", AlertStyle.Success);
|
||||
}
|
||||
|
||||
void OnChatCleared()
|
||||
{
|
||||
console.Log("Chat cleared", AlertStyle.Warning);
|
||||
}
|
||||
}
|
||||
40
RadzenBlazorDemos/Pages/AIChatCustomStyling.razor
Normal file
40
RadzenBlazorDemos/Pages/AIChatCustomStyling.razor
Normal file
@@ -0,0 +1,40 @@
|
||||
@using Radzen
|
||||
@using Radzen.Blazor
|
||||
|
||||
<RadzenStack class="rz-p-0 rz-p-md-12">
|
||||
<RadzenCard class="rz-p-4" Variant="Variant.Outlined">
|
||||
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Start" Wrap="FlexWrap.Wrap" Gap="1rem">
|
||||
<RadzenStack Orientation="Orientation.Vertical" Gap="8px">
|
||||
<RadzenLabel Text="Show Clear Button" />
|
||||
<RadzenSwitch @bind-Value="showClearButton" />
|
||||
</RadzenStack>
|
||||
|
||||
<RadzenStack Orientation="Orientation.Vertical" Gap="8px">
|
||||
<RadzenLabel Text="Disabled" />
|
||||
<RadzenSwitch @bind-Value="isDisabled" />
|
||||
</RadzenStack>
|
||||
|
||||
<RadzenStack Orientation="Orientation.Vertical" Gap="8px">
|
||||
<RadzenLabel Text="Read Only" />
|
||||
<RadzenSwitch @bind-Value="isReadOnly" />
|
||||
</RadzenStack>
|
||||
</RadzenStack>
|
||||
</RadzenCard>
|
||||
<RadzenAIChat @ref="customChat"
|
||||
Title="Custom Assistant"
|
||||
Placeholder="Type your message here..."
|
||||
EmptyMessage="Welcome! Start chatting with me."
|
||||
ShowClearButton="@showClearButton"
|
||||
Disabled="@isDisabled"
|
||||
ReadOnly="@isReadOnly"
|
||||
Style="height: 400px; border: 2px solid var(--rz-primary);"
|
||||
/>
|
||||
</RadzenStack>
|
||||
|
||||
@code {
|
||||
RadzenAIChat customChat;
|
||||
|
||||
private bool showClearButton = true;
|
||||
private bool isDisabled = false;
|
||||
private bool isReadOnly = false;
|
||||
}
|
||||
42
RadzenBlazorDemos/Pages/AIChatEvents.razor
Normal file
42
RadzenBlazorDemos/Pages/AIChatEvents.razor
Normal file
@@ -0,0 +1,42 @@
|
||||
@using Radzen
|
||||
@using Radzen.Blazor
|
||||
|
||||
<RadzenStack class="rz-p-0 rz-p-md-12">
|
||||
<RadzenAIChat @ref="eventsChat"
|
||||
Title="Event Demo"
|
||||
Placeholder="Type a message to see events..."
|
||||
Style="height: 400px;"
|
||||
MessageAdded="@OnMessageAdded"
|
||||
MessageSent="@OnMessageSent"
|
||||
ResponseReceived="@OnResponseReceived"
|
||||
ChatCleared="@OnChatCleared"
|
||||
/>
|
||||
</RadzenStack>
|
||||
|
||||
<EventConsole @ref="console" Style="min-height: 200px;" />
|
||||
|
||||
@code {
|
||||
RadzenAIChat eventsChat;
|
||||
EventConsole console;
|
||||
|
||||
void OnMessageAdded(Radzen.Blazor.ChatMessage message)
|
||||
{
|
||||
console.Log($"Message added: {(message.IsUser ? "User" : "Assistant")} - {message.Content.Substring(0, Math.Min(50, message.Content.Length))}...",
|
||||
message.IsUser ? AlertStyle.Info : AlertStyle.Success);
|
||||
}
|
||||
|
||||
void OnMessageSent(string message)
|
||||
{
|
||||
console.Log($"Message sent: {message}", AlertStyle.Info);
|
||||
}
|
||||
|
||||
void OnResponseReceived(string response)
|
||||
{
|
||||
console.Log($"AI Response received: {response.Substring(0, Math.Min(50, response.Length))}...", AlertStyle.Success);
|
||||
}
|
||||
|
||||
void OnChatCleared()
|
||||
{
|
||||
console.Log("Chat cleared", AlertStyle.Warning);
|
||||
}
|
||||
}
|
||||
59
RadzenBlazorDemos/Pages/AIChatPage.razor
Normal file
59
RadzenBlazorDemos/Pages/AIChatPage.razor
Normal file
@@ -0,0 +1,59 @@
|
||||
@page "/aichat"
|
||||
@page "/docs/guides/components/aichat.html"
|
||||
|
||||
@using Radzen
|
||||
@using Radzen.Blazor
|
||||
|
||||
<RadzenText TextStyle="TextStyle.H2" TagName="TagName.H1" class="rz-pt-8">
|
||||
AIChat
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" TagName="TagName.P" class="rz-pb-4">
|
||||
The component requires the <strong>IAIChatService</strong> to be registered in your application, please use our demos source for reference.
|
||||
This service supports any OpenAI-compatible endpoint, including OpenAI, Azure OpenAI, Cloudflare AI, and other compatible providers.
|
||||
For WebAssembly applications, you'll need to use a server-side proxy endpoint since browsers cannot make direct requests to external APIs due to CORS restrictions, refer to AIChatController.
|
||||
</RadzenText>
|
||||
|
||||
<RadzenExample ComponentName="AIChat" Example="AIChatConfig">
|
||||
<AIChatConfig />
|
||||
</RadzenExample>
|
||||
|
||||
<RadzenText Anchor="aichat#custom-styling" TextStyle="TextStyle.H5" TagName="TagName.H2" class="rz-pt-12">
|
||||
Custom styling
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Body1" class="rz-mb-8">
|
||||
Customize the appearance of the <strong>AIChat</strong> component using <code>Style</code>, <code>ShowClearButton</code>, <code>Disabled</code>, and <code>ReadOnly</code> properties.
|
||||
</RadzenText>
|
||||
<RadzenExample ComponentName="AIChat" Example="AIChatCustomStyling">
|
||||
<AIChatCustomStyling />
|
||||
</RadzenExample>
|
||||
|
||||
<RadzenText Anchor="aichat#compact-aichat" TextStyle="TextStyle.H5" TagName="TagName.H2" class="rz-pt-12">
|
||||
Compact aichat
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Body1" class="rz-mb-8">
|
||||
Create a more compact chat interface suitable for smaller spaces or sidebar implementations.
|
||||
</RadzenText>
|
||||
<RadzenExample ComponentName="AIChat" Example="AIChatCompact">
|
||||
<AIChatCompact />
|
||||
</RadzenExample>
|
||||
|
||||
<RadzenText Anchor="aichat#events" TextStyle="TextStyle.H5" TagName="TagName.H2" class="rz-pt-12">
|
||||
Events and interactions
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Body1" class="rz-mb-8">
|
||||
Handle chat events like <code>MessageAdded</code>, <code>MessageSent</code>, <code>ResponseReceived</code>, and <code>ChatCleared</code> to integrate with your application logic.
|
||||
</RadzenText>
|
||||
<RadzenExample ComponentName="AIChat" Example="AIChatEvents">
|
||||
<AIChatEvents />
|
||||
</RadzenExample>
|
||||
|
||||
<RadzenText Anchor="aichat#memory" TextStyle="TextStyle.H5" TagName="TagName.H2" class="rz-pt-12">
|
||||
AI Chat with Memory
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Body1" class="rz-mb-8">
|
||||
The <strong>AIChat</strong> component supports conversation memory that remembers previous questions and maintains context across multiple interactions.
|
||||
Use <code>SessionId</code> to maintain conversation state and <code>SessionIdChanged</code> to track session changes.
|
||||
</RadzenText>
|
||||
<RadzenExample ComponentName="AIChat" Example="AIChatWithMemory">
|
||||
<AIChatWithMemory />
|
||||
</RadzenExample>
|
||||
170
RadzenBlazorDemos/Pages/AIChatWithMemory.razor
Normal file
170
RadzenBlazorDemos/Pages/AIChatWithMemory.razor
Normal file
@@ -0,0 +1,170 @@
|
||||
@using Radzen.Blazor
|
||||
@using Radzen
|
||||
@inherits ComponentBase
|
||||
|
||||
<RadzenRow Gap="1rem">
|
||||
<RadzenColumn Size="12" SizeLG="6">
|
||||
<RadzenAIChat Title="AI Assistant with Memory"
|
||||
Placeholder="Ask me anything... I'll remember our conversation!"
|
||||
SessionId="@currentSessionId"
|
||||
SessionIdChanged="@OnSessionIdChanged"
|
||||
MessageAdded="@OnMessageAdded"
|
||||
ResponseReceived="@OnResponseReceived"
|
||||
ShowClearButton="true"
|
||||
class="rz-h-100" />
|
||||
</RadzenColumn>
|
||||
<RadzenColumn Size="12" SizeLG="6">
|
||||
<RadzenRow>
|
||||
<RadzenColumn Size="12" SizeLG="12">
|
||||
<RadzenCard Variant="Variant.Outlined">
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1">Memory Statistics</RadzenText>
|
||||
<RadzenRow>
|
||||
<RadzenColumn Size="6">
|
||||
<RadzenText TextStyle="TextStyle.H4">@activeSessionsCount</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Caption">Active Sessions</RadzenText>
|
||||
</RadzenColumn>
|
||||
<RadzenColumn Size="6">
|
||||
<RadzenText TextStyle="TextStyle.H4">@totalMessagesCount</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Caption">Total Messages</RadzenText>
|
||||
</RadzenColumn>
|
||||
</RadzenRow>
|
||||
</RadzenCard>
|
||||
</RadzenColumn>
|
||||
<RadzenColumn Size="12">
|
||||
<RadzenCard Variant="Variant.Outlined">
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1">Session Management</RadzenText>
|
||||
<RadzenStack Orientation="Orientation.Vertical">
|
||||
<RadzenStack Orientation="Orientation.Horizontal" Wrap="FlexWrap.Wrap">
|
||||
<RadzenButton Text="New Session" Icon="add" ButtonStyle="ButtonStyle.Primary" Click="@OnNewSession" Variant="Variant.Flat" />
|
||||
<RadzenButton Text="Load Session" Icon="folder_open" ButtonStyle="ButtonStyle.Base" Click="@OnLoadSession" Variant="Variant.Flat" />
|
||||
<RadzenButton Text="Clear Session" Icon="clear_all" ButtonStyle="ButtonStyle.Danger" Click="@OnClearSession" Variant="Variant.Flat" />
|
||||
</RadzenStack>
|
||||
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Wrap="FlexWrap.Wrap">
|
||||
<RadzenLabel Style="white-space: nowrap;">Current Session ID:</RadzenLabel>
|
||||
<RadzenTextBox @bind-Value="@currentSessionId" ReadOnly="true" Style="flex: 1; min-width: 360px;" />
|
||||
</RadzenStack>
|
||||
</RadzenStack>
|
||||
</RadzenCard>
|
||||
</RadzenColumn>
|
||||
<RadzenColumn Size="12">
|
||||
<RadzenCard Variant="Variant.Outlined">
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1">Session History</RadzenText>
|
||||
<RadzenStack Orientation="Orientation.Vertical">
|
||||
<RadzenDropDown TValue="string" Data="@sessionIds" @bind-Value="@selectedSessionId" Placeholder="Select a session to load" Style="width: 100%;" />
|
||||
<RadzenButton Text="Load Selected Session" Icon="folder_open" ButtonStyle="ButtonStyle.Base" Variant="Variant.Flat"
|
||||
Click="@OnLoadSelectedSession" Style="width: 100%;" Disabled="@(string.IsNullOrEmpty(selectedSessionId))" />
|
||||
</RadzenStack>
|
||||
</RadzenCard>
|
||||
</RadzenColumn>
|
||||
</RadzenRow>
|
||||
</RadzenColumn>
|
||||
<RadzenColumn Size="12">
|
||||
<RadzenCard Variant="Variant.Outlined">
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1">Memory Features</RadzenText>
|
||||
<RadzenRow>
|
||||
<RadzenColumn Size="12" SizeMD="6">
|
||||
<RadzenText TextStyle="TextStyle.H6">Conversation Memory</RadzenText>
|
||||
<ul>
|
||||
<li>The AI remembers all previous messages in the conversation</li>
|
||||
<li>Context is maintained across multiple questions</li>
|
||||
<li>You can ask follow-up questions naturally</li>
|
||||
<li>The AI can reference previous parts of the conversation</li>
|
||||
</ul>
|
||||
</RadzenColumn>
|
||||
<RadzenColumn Size="12" SizeMD="6">
|
||||
<RadzenText TextStyle="TextStyle.H6">Session Management</RadzenText>
|
||||
<ul>
|
||||
<li>Create new sessions for different conversations</li>
|
||||
<li>Switch between different conversation contexts</li>
|
||||
<li>Clear sessions to start fresh</li>
|
||||
<li>Session data persists during the application session</li>
|
||||
</ul>
|
||||
</RadzenColumn>
|
||||
</RadzenRow>
|
||||
</RadzenCard>
|
||||
</RadzenColumn>
|
||||
</RadzenRow>
|
||||
|
||||
@code {
|
||||
private string currentSessionId;
|
||||
private string selectedSessionId;
|
||||
private List<string> sessionIds = new();
|
||||
private int activeSessionsCount = 0;
|
||||
private int totalMessagesCount = 0;
|
||||
|
||||
[Inject]
|
||||
private IAIChatService ChatService { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Create a new session by default
|
||||
currentSessionId = Guid.NewGuid().ToString();
|
||||
|
||||
// Update session statistics
|
||||
await UpdateSessionStatistics();
|
||||
}
|
||||
|
||||
private async Task OnSessionIdChanged(string sessionId)
|
||||
{
|
||||
currentSessionId = sessionId;
|
||||
await UpdateSessionStatistics();
|
||||
}
|
||||
|
||||
private async Task OnMessageAdded(Radzen.Blazor.ChatMessage message)
|
||||
{
|
||||
// Update statistics when a message is added
|
||||
await UpdateSessionStatistics();
|
||||
}
|
||||
|
||||
private async Task OnResponseReceived(string response)
|
||||
{
|
||||
// Update statistics when a response is received
|
||||
await UpdateSessionStatistics();
|
||||
}
|
||||
|
||||
private async Task OnNewSession()
|
||||
{
|
||||
currentSessionId = Guid.NewGuid().ToString();
|
||||
await UpdateSessionStatistics();
|
||||
}
|
||||
|
||||
private async Task OnLoadSession()
|
||||
{
|
||||
// This would typically open a dialog to select a session
|
||||
// For demo purposes, we'll just create a new session
|
||||
currentSessionId = Guid.NewGuid().ToString();
|
||||
await UpdateSessionStatistics();
|
||||
}
|
||||
|
||||
private async Task OnClearSession()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(currentSessionId))
|
||||
{
|
||||
ChatService.ClearSession(currentSessionId);
|
||||
await UpdateSessionStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnLoadSelectedSession()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(selectedSessionId))
|
||||
{
|
||||
currentSessionId = selectedSessionId;
|
||||
await UpdateSessionStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateSessionStatistics()
|
||||
{
|
||||
var sessions = ChatService.GetActiveSessions().ToList();
|
||||
activeSessionsCount = sessions.Count;
|
||||
totalMessagesCount = sessions.Sum(s => s.Messages.Count);
|
||||
|
||||
// Update session IDs list
|
||||
sessionIds = sessions.Select(s => s.Id).ToList();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using RadzenBlazorDemos.Services;
|
||||
using Radzen;
|
||||
using RadzenBlazorDemos.Data;
|
||||
using RadzenBlazorDemos;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
@@ -23,4 +24,12 @@ builder.Services.AddScoped<NorthwindService>();
|
||||
builder.Services.AddScoped<NorthwindODataService>();
|
||||
builder.Services.AddSingleton<GitHubService>();
|
||||
|
||||
builder.Services.AddAIChatService(options =>
|
||||
{
|
||||
options.Proxy = "api/chat/completions";
|
||||
options.Model = "@cf/meta/llama-3.1-8b-instruct";
|
||||
options.SystemPrompt = "You are a helpful AI code assistant.";
|
||||
options.Temperature = 0.7;
|
||||
});
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -1737,6 +1737,15 @@ namespace RadzenBlazorDemos
|
||||
Tags = new [] { "input", "form", "edit" }
|
||||
},
|
||||
new Example
|
||||
{
|
||||
Name = "AIChat",
|
||||
Path = "aichat",
|
||||
New = true,
|
||||
Description = "A modern chat component with AI integration that provides a conversational interface similar to popular chat applications.",
|
||||
Icon = "\ue0b7",
|
||||
Tags = new [] { "chat", "ai", "conversation", "message", "streaming" }
|
||||
},
|
||||
new Example
|
||||
{
|
||||
Name = "TextBox",
|
||||
Path = "textbox",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
|
||||
Reference in New Issue
Block a user