mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-02-10 17:49:07 +00:00
Added chat UI
This commit is contained in:
parent
862ec9c36a
commit
bb663d1b73
64
app/MindWork AI Studio/Chat/ChatRole.cs
Normal file
64
app/MindWork AI Studio/Chat/ChatRole.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using MudBlazor;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Possible roles in the chat.
|
||||
/// </summary>
|
||||
public enum ChatRole
|
||||
{
|
||||
NONE,
|
||||
UNKNOWN,
|
||||
|
||||
SYSTEM,
|
||||
USER,
|
||||
AI,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for the ChatRole enum.
|
||||
/// </summary>
|
||||
public static class ExtensionsChatRole
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the name of the role.
|
||||
/// </summary>
|
||||
/// <param name="role">The role.</param>
|
||||
/// <returns>The name of the role.</returns>
|
||||
public static string ToName(this ChatRole role) => role switch
|
||||
{
|
||||
ChatRole.SYSTEM => "System",
|
||||
ChatRole.USER => "You",
|
||||
ChatRole.AI => "AI",
|
||||
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the color of the role.
|
||||
/// </summary>
|
||||
/// <param name="role">The role.</param>
|
||||
/// <returns>The color of the role.</returns>
|
||||
public static Color ToColor(this ChatRole role) => role switch
|
||||
{
|
||||
ChatRole.SYSTEM => Color.Info,
|
||||
ChatRole.USER => Color.Primary,
|
||||
ChatRole.AI => Color.Tertiary,
|
||||
|
||||
_ => Color.Error,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the icon of the role.
|
||||
/// </summary>
|
||||
/// <param name="role">The role.</param>
|
||||
/// <returns>The icon of the role.</returns>
|
||||
public static string ToIcon(this ChatRole role) => role switch
|
||||
{
|
||||
ChatRole.SYSTEM => Icons.Material.Filled.Settings,
|
||||
ChatRole.USER => Icons.Material.Filled.Person,
|
||||
ChatRole.AI => Icons.Material.Filled.AutoAwesome,
|
||||
|
||||
_ => Icons.Material.Filled.Help,
|
||||
};
|
||||
}
|
31
app/MindWork AI Studio/Chat/ChatThread.cs
Normal file
31
app/MindWork AI Studio/Chat/ChatThread.cs
Normal file
@ -0,0 +1,31 @@
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Data structure for a chat thread.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the chat thread.</param>
|
||||
/// <param name="seed">The seed for the chat thread. Some providers use this to generate deterministic results.</param>
|
||||
/// <param name="systemPrompt">The system prompt for the chat thread.</param>
|
||||
/// <param name="blocks">The content blocks of the chat thread.</param>
|
||||
public sealed class ChatThread(string name, int seed, string systemPrompt, IEnumerable<ContentBlock> blocks)
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// The seed for the chat thread. Some providers use this to generate deterministic results.
|
||||
/// </summary>
|
||||
public int Seed { get; set; } = seed;
|
||||
|
||||
/// <summary>
|
||||
/// The current system prompt for the chat thread.
|
||||
/// </summary>
|
||||
public string SystemPrompt { get; set; } = systemPrompt;
|
||||
|
||||
/// <summary>
|
||||
/// The content blocks of the chat thread.
|
||||
/// </summary>
|
||||
public List<ContentBlock> Blocks { get; init; } = blocks.ToList();
|
||||
}
|
30
app/MindWork AI Studio/Chat/ContentBlock.cs
Normal file
30
app/MindWork AI Studio/Chat/ContentBlock.cs
Normal file
@ -0,0 +1,30 @@
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// A block of content in a chat thread. Might be any type of content, e.g., text, image, voice, etc.
|
||||
/// </summary>
|
||||
/// <param name="time">Time when the content block was created.</param>
|
||||
/// <param name="type">Type of the content block, e.g., text, image, voice, etc.</param>
|
||||
/// <param name="content">The content of the block.</param>
|
||||
public class ContentBlock(DateTimeOffset time, ContentType type, IContent content)
|
||||
{
|
||||
/// <summary>
|
||||
/// Time when the content block was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset Time => time;
|
||||
|
||||
/// <summary>
|
||||
/// Type of the content block, e.g., text, image, voice, etc.
|
||||
/// </summary>
|
||||
public ContentType ContentType => type;
|
||||
|
||||
/// <summary>
|
||||
/// The content of the block.
|
||||
/// </summary>
|
||||
public IContent Content => content;
|
||||
|
||||
/// <summary>
|
||||
/// The role of the content block in the chat thread, e.g., user, AI, etc.
|
||||
/// </summary>
|
||||
public ChatRole Role { get; init; } = ChatRole.NONE;
|
||||
}
|
@ -3,16 +3,58 @@
|
||||
<MudCard Class="my-2 rounded-lg" Outlined="@true">
|
||||
<MudCardHeader>
|
||||
<CardHeaderAvatar>
|
||||
<MudAvatar Color="Color.Primary">Y</MudAvatar>
|
||||
<MudAvatar Color="@this.Role.ToColor()">
|
||||
<MudIcon Icon="@this.Role.ToIcon()"/>
|
||||
</MudAvatar>
|
||||
</CardHeaderAvatar>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.body1">You</MudText>
|
||||
<MudText Typo="Typo.body1">@this.Role.ToName() (@this.Time)</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard" />
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.body2">bla bla bla</MudText>
|
||||
@if (!this.HideContent)
|
||||
{
|
||||
if (this.Content.IsStreaming)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
|
||||
}
|
||||
|
||||
switch (this.Type)
|
||||
{
|
||||
case ContentType.TEXT:
|
||||
if (this.Content is ContentText textContent)
|
||||
{
|
||||
if (textContent.InitialRemoteWait)
|
||||
{
|
||||
<MudSkeleton Width="30%" Height="42px;"/>
|
||||
<MudSkeleton Width="80%"/>
|
||||
<MudSkeleton Width="100%"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudMarkdown Value="@textContent.Text"/>
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case ContentType.IMAGE:
|
||||
if (this.Content is ContentImage imageContent)
|
||||
{
|
||||
<MudImage Src="@imageContent.URL"/>
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
<MudText Typo="Typo.body2">
|
||||
Cannot render content of type @this.Type yet.
|
||||
</MudText>
|
||||
break;
|
||||
}
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
@ -1,7 +1,113 @@
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
using MudBlazor;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// The UI component for a chat content block, i.e., for any IContent.
|
||||
/// </summary>
|
||||
public partial class ContentBlockComponent : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The role of the chat content block.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public ChatRole Role { get; init; } = ChatRole.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The content.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IContent Content { get; init; } = new ContentText();
|
||||
|
||||
/// <summary>
|
||||
/// The content type.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public ContentType Type { get; init; } = ContentType.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// When was the content created?
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public DateTimeOffset Time { get; init; }
|
||||
|
||||
[Inject]
|
||||
private Rust Rust { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
private bool HideContent { get; set; }
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Register the streaming events:
|
||||
this.Content.StreamingDone = this.AfterStreaming;
|
||||
this.Content.StreamingEvent = () => this.InvokeAsync(this.StateHasChanged);
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets called when the content stream ended.
|
||||
/// </summary>
|
||||
private async Task AfterStreaming()
|
||||
{
|
||||
// Might be called from a different thread, so we need to invoke the UI thread:
|
||||
await this.InvokeAsync(() =>
|
||||
{
|
||||
//
|
||||
// Issue we try to solve: When the content changes during streaming,
|
||||
// Blazor might fail to see all changes made to the render tree.
|
||||
// This happens mostly when Markdown code blocks are streamed.
|
||||
//
|
||||
|
||||
// Hide the content for a short time:
|
||||
this.HideContent = true;
|
||||
|
||||
// Let Blazor update the UI, i.e., to see the render tree diff:
|
||||
this.StateHasChanged();
|
||||
|
||||
// Show the content again:
|
||||
this.HideContent = false;
|
||||
|
||||
// Let Blazor update the UI, i.e., to see the render tree diff:
|
||||
this.StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Copy this block's content to the clipboard.
|
||||
/// </summary>
|
||||
private async Task CopyToClipboard()
|
||||
{
|
||||
switch (this.Type)
|
||||
{
|
||||
case ContentType.TEXT:
|
||||
var textContent = (ContentText) this.Content;
|
||||
await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, textContent.Text);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.Snackbar.Add("Cannot copy this content type to clipboard!", Severity.Error, config =>
|
||||
{
|
||||
config.Icon = Icons.Material.Filled.ContentCopy;
|
||||
config.IconSize = Size.Large;
|
||||
config.IconColor = Color.Error;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
44
app/MindWork AI Studio/Chat/ContentImage.cs
Normal file
44
app/MindWork AI Studio/Chat/ContentImage.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an image inside the chat.
|
||||
/// </summary>
|
||||
public sealed class ContentImage : IContent
|
||||
{
|
||||
#region Implementation of IContent
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool InitialRemoteWait { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsStreaming { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Func<Task> StreamingDone { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the image.
|
||||
/// </summary>
|
||||
public string URL { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The local path of the image.
|
||||
/// </summary>
|
||||
public string LocalPath { get; set; } = string.Empty;
|
||||
}
|
102
app/MindWork AI Studio/Chat/ContentText.cs
Normal file
102
app/MindWork AI Studio/Chat/ContentText.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Text content in the chat.
|
||||
/// </summary>
|
||||
public sealed class ContentText : IContent
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum time between two streaming events, when the user
|
||||
/// enables the energy saving mode.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan MIN_TIME = TimeSpan.FromSeconds(3);
|
||||
|
||||
#region Implementation of IContent
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool InitialRemoteWait { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsStreaming { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Func<Task> StreamingDone { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default)
|
||||
{
|
||||
if(chatThread is null)
|
||||
return;
|
||||
|
||||
// Store the last time we got a response. We use this later,
|
||||
// to determine whether we should notify the UI about the
|
||||
// new content or not. Depends on the energy saving mode
|
||||
// the user chose.
|
||||
var last = DateTimeOffset.Now;
|
||||
|
||||
// Start another thread by using a task, to uncouple
|
||||
// the UI thread from the AI processing:
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
// We show the waiting animation until we get the first response:
|
||||
this.InitialRemoteWait = true;
|
||||
|
||||
// Iterate over the responses from the AI:
|
||||
await foreach (var deltaText in provider.StreamChatCompletion(jsRuntime, settings, chatModel, chatThread, token))
|
||||
{
|
||||
// When the user cancels the request, we stop the loop:
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// Stop the waiting animation:
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = true;
|
||||
|
||||
// Add the response to the text:
|
||||
this.Text += deltaText;
|
||||
|
||||
// Notify the UI that the content has changed,
|
||||
// depending on the energy saving mode:
|
||||
var now = DateTimeOffset.Now;
|
||||
switch (settings.ConfigurationData.IsSavingEnergy)
|
||||
{
|
||||
// Energy saving mode is off. We notify the UI
|
||||
// as fast as possible -- no matter the odds:
|
||||
case false:
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
|
||||
// Energy saving mode is on. We notify the UI
|
||||
// only when the time between two events is
|
||||
// greater than the minimum time:
|
||||
case true when now - last > MIN_TIME:
|
||||
last = now;
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the waiting animation (in case the loop
|
||||
// was stopped or no content was received):
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = false;
|
||||
}, token);
|
||||
|
||||
// Inform the UI that the streaming is done:
|
||||
await this.StreamingDone();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The text content.
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
16
app/MindWork AI Studio/Chat/ContentType.cs
Normal file
16
app/MindWork AI Studio/Chat/ContentType.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// The content type of messages.
|
||||
/// </summary>
|
||||
public enum ContentType
|
||||
{
|
||||
NONE,
|
||||
UNKNOWN,
|
||||
|
||||
TEXT,
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
SPEECH,
|
||||
}
|
40
app/MindWork AI Studio/Chat/IContent.cs
Normal file
40
app/MindWork AI Studio/Chat/IContent.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// The interface for any content in the chat.
|
||||
/// </summary>
|
||||
public interface IContent
|
||||
{
|
||||
/// <summary>
|
||||
/// Do we need to wait for the remote, i.e., the AI, to process the related request?
|
||||
/// Does not indicate that the stream is finished; it only indicates that we are
|
||||
/// waiting for the first response, i.e., wait for the remote to pick up the request.
|
||||
/// </summary>
|
||||
public bool InitialRemoteWait { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the content is streaming right now. False, if the content is
|
||||
/// either static or the stream has finished.
|
||||
/// </summary>
|
||||
public bool IsStreaming { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An action that is called when the content was changed during streaming.
|
||||
/// </summary>
|
||||
public Func<Task> StreamingEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An action that is called when the streaming is done.
|
||||
/// </summary>
|
||||
public Func<Task> StreamingDone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Uses the provider to create the content.
|
||||
/// </summary>
|
||||
public Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default);
|
||||
}
|
12
app/MindWork AI Studio/Chat/Workspace.cs
Normal file
12
app/MindWork AI Studio/Chat/Workspace.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Data about a workspace.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the workspace.</param>
|
||||
public sealed class Workspace(string name)
|
||||
{
|
||||
public string Name { get; set; } = name;
|
||||
|
||||
public List<ChatThread> Threads { get; set; } = new();
|
||||
}
|
@ -1,26 +1,27 @@
|
||||
@page "/chat"
|
||||
@using AIStudio.Chat
|
||||
@using AIStudio.Settings
|
||||
|
||||
<MudText Typo="Typo.h3" Class="mb-12">Chats</MudText>
|
||||
<MudText Typo="Typo.h3" Class="mb-2">Chats</MudText>
|
||||
<MudSelect T="Provider" @bind-Value="@this.selectedProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-2 rounded-lg" Variant="Variant.Outlined">
|
||||
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
|
||||
{
|
||||
<MudSelectItem Value="@provider"/>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<div class="d-flex flex-column" style="height: calc(100vh - 8.3em);">
|
||||
<div class="d-flex flex-column" style="height: calc(100vh - 12.3em);">
|
||||
<div class="flex-auto overflow-auto">
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
<ContentBlockComponent/>
|
||||
@if (this.chatThread is not null)
|
||||
{
|
||||
foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time))
|
||||
{
|
||||
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudPaper Style="flex: 0 0 auto;">
|
||||
<MudTextField T="string" Variant="Variant.Filled" AutoGrow="@true" Lines="3" MaxLines="12" Label="Your Prompt" Placeholder="Type your question here..." Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Send" OnAdornmentClick="() => this.SendMessage()">
|
||||
</MudTextField>
|
||||
<MudTextField T="string" @ref="@this.inputField" @bind-Text="@this.userInput" Variant="Variant.Outlined" AutoGrow="@true" Lines="3" MaxLines="12" Label="@this.InputLabel" Placeholder="@this.ProviderPlaceholder" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Send" OnAdornmentClick="() => this.SendMessage()" ReadOnly="!this.IsProviderSelected || this.isStreaming" Immediate="@true" OnKeyUp="this.InputKeyEvent"/>
|
||||
</MudPaper>
|
||||
</div>
|
@ -1,4 +1,12 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
using MudBlazor;
|
||||
|
||||
namespace AIStudio.Components.Pages;
|
||||
|
||||
@ -7,7 +15,122 @@ namespace AIStudio.Components.Pages;
|
||||
/// </summary>
|
||||
public partial class Chat : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
public IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
public Random RNG { get; set; } = null!;
|
||||
|
||||
private AIStudio.Settings.Provider selectedProvider;
|
||||
private ChatThread? chatThread;
|
||||
private bool isStreaming;
|
||||
private string userInput = string.Empty;
|
||||
|
||||
// Unfortunately, we need the input field reference to clear it after sending a message.
|
||||
// This is necessary because we have to handle the key events ourselves. Otherwise,
|
||||
// the clearing would be done automatically.
|
||||
private MudTextField<string> inputField = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Ensure that the settings are loaded:
|
||||
await this.SettingsManager.LoadSettings();
|
||||
|
||||
// For now, we just create a new chat thread.
|
||||
// Later we want the chats to be persisted
|
||||
// across page loads and organize them in
|
||||
// a chat history & workspaces.
|
||||
this.chatThread = new("Thread 1", this.RNG.Next(), "You are a helpful assistant!", []);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool IsProviderSelected => this.selectedProvider.UsedProvider != Providers.NONE;
|
||||
|
||||
private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first";
|
||||
|
||||
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.selectedProvider.InstanceName}', provider '{this.selectedProvider.UsedProvider.ToName()}')" : "Select a provider first";
|
||||
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (!this.IsProviderSelected)
|
||||
return;
|
||||
|
||||
//
|
||||
// Add the user message to the thread:
|
||||
//
|
||||
var time = DateTimeOffset.Now;
|
||||
this.chatThread?.Blocks.Add(new ContentBlock(time, ContentType.TEXT, new ContentText
|
||||
{
|
||||
// Text content properties:
|
||||
Text = this.userInput,
|
||||
})
|
||||
{
|
||||
// Content block properties:
|
||||
Role = ChatRole.USER,
|
||||
});
|
||||
|
||||
//
|
||||
// Add the AI response to the thread:
|
||||
//
|
||||
var aiText = new ContentText
|
||||
{
|
||||
// We have to wait for the remote
|
||||
// for the content stream:
|
||||
InitialRemoteWait = true,
|
||||
};
|
||||
|
||||
this.chatThread?.Blocks.Add(new ContentBlock(time, ContentType.TEXT, aiText)
|
||||
{
|
||||
Role = ChatRole.AI,
|
||||
});
|
||||
|
||||
// Clear the input field:
|
||||
await this.inputField.Clear();
|
||||
this.userInput = string.Empty;
|
||||
|
||||
// Enable the stream state for the chat component:
|
||||
this.isStreaming = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(), this.JsRuntime, this.SettingsManager, new Model("gpt-4-turbo-preview"), this.chatThread);
|
||||
|
||||
// Disable the stream state:
|
||||
this.isStreaming = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
|
||||
{
|
||||
var key = keyEvent.Code.ToLowerInvariant();
|
||||
|
||||
// Was the enter key (either enter or numpad enter) pressed?
|
||||
var isEnter = key is "enter" or "numpadenter";
|
||||
|
||||
// Was a modifier key pressed as well?
|
||||
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
|
||||
|
||||
// Depending on the user's settings, might react to shortcuts:
|
||||
switch (this.SettingsManager.ConfigurationData.ShortcutSendBehavior)
|
||||
{
|
||||
case SendBehavior.ENTER_IS_SENDING:
|
||||
if (!isModifier && isEnter)
|
||||
await this.SendMessage();
|
||||
break;
|
||||
|
||||
case SendBehavior.MODIFER_ENTER_IS_SENDING:
|
||||
if (isEnter && isModifier)
|
||||
await this.SendMessage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user