mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-07 22:09:40 +00:00
Added chat UI
This commit is contained in:
12 files changed
+631
-20
No files matched your search
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// The content type of messages.
|
||||
/// </summary>
|
||||
public enum ContentType
|
||||
{
|
||||
NONE,
|
||||
UNKNOWN,
|
||||
|
||||
TEXT,
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
SPEECH,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user