mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 03:37:59 +00:00
Merge branch 'main' into chunk-data
This commit is contained in:
commit
00b6c98f3b
167 files changed
+8153
-3577
No files matched your search
@@ -13,6 +13,11 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (240, "v26.5.5, build 240 (2026-05-25 18:52 UTC)", "v26.5.5.md"),
|
||||
new (239, "v26.5.4, build 239 (2026-05-13 11:58 UTC)", "v26.5.4.md"),
|
||||
new (238, "v26.5.3, build 238 (2026-05-13 09:50 UTC)", "v26.5.3.md"),
|
||||
new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"),
|
||||
new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"),
|
||||
new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"),
|
||||
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
|
||||
new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"),
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<UserPromptComponent
|
||||
T="string"
|
||||
@ref="@this.inputField"
|
||||
@bind-Text="@this.userInput"
|
||||
@bind-Text="@this.UserInput"
|
||||
Variant="Variant.Outlined"
|
||||
AutoGrow="@true"
|
||||
Lines="3"
|
||||
@@ -71,7 +71,7 @@
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
||||
{
|
||||
<MudTooltip Text="@T("Save chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.isStreaming)"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@@ -92,35 +92,35 @@
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
<MudTooltip Text="@T("Delete this chat & start a new one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="@(() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true))" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="@(() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true))" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
<MudTooltip Text="@T("Move the chat to a workspace, or to another if it is already in one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="@(() => this.MoveChatToWorkspace())"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)" OnClick="@this.MoveChatToWorkspace"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" @bind-DocumentPaths="@this.chatDocumentPaths" CatchAllDocuments="true" UseSmallForm="true" Provider="@this.Provider"/>
|
||||
<AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" Provider="@this.Provider"/>
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
|
||||
<MudTooltip Text="@T("Bold")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatBold" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_BOLD)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatBold" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_BOLD))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Italic")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatItalic" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_ITALIC)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatItalic" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_ITALIC))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Heading")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.TextFields" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_HEADING)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.TextFields" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_HEADING))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Bulleted List")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatListBulleted" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_BULLET_LIST)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatListBulleted" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_BULLET_LIST))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Code")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Code" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_CODE)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Code" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_CODE))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
@@ -137,10 +137,10 @@
|
||||
<ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
|
||||
}
|
||||
|
||||
@if (this.isStreaming && this.cancellationTokenSource is not null)
|
||||
@if (this.IsCurrentChatStreaming)
|
||||
{
|
||||
<MudTooltip Text="@T("Stop generation")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="@(() => this.CancelStreaming())"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="@this.CancelStreaming"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
@@ -38,6 +38,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
[Parameter]
|
||||
public Workspaces? Workspaces { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public ChatComposerState ComposerState { get; set; } = new();
|
||||
|
||||
[Inject]
|
||||
private ILogger<ChatComponent> Logger { get; set; } = null!;
|
||||
@@ -50,6 +53,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AIJobService AIJobService { get; init; } = null!;
|
||||
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
@@ -61,8 +67,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
private bool mustScrollToBottomAfterRender;
|
||||
private InnerScrolling scrollingArea = null!;
|
||||
private byte scrollRenderCountdown;
|
||||
private bool isStreaming;
|
||||
private string userInput = string.Empty;
|
||||
private bool mustStoreChat;
|
||||
private bool mustLoadChat;
|
||||
private LoadChat loadChat;
|
||||
@@ -70,6 +74,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Guid currentWorkspaceId = Guid.Empty;
|
||||
private Guid currentChatThreadId = Guid.Empty;
|
||||
private Guid loadedParameterChatId = Guid.Empty;
|
||||
private Guid loadedParameterWorkspaceId = Guid.Empty;
|
||||
private Guid foregroundChatId = Guid.Empty;
|
||||
private int workspaceHeaderSyncVersion;
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private HashSet<FileAttachment> chatDocumentPaths = [];
|
||||
@@ -80,12 +87,27 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// this, we cannot clear the input field.
|
||||
private UserPromptComponent<string> inputField = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the user's input in the chat interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property serves as a bridge between the chat component and the
|
||||
/// underlying composer state, allowing user input to be dynamically updated
|
||||
/// and managed. The setter also triggers state changes within the composer
|
||||
/// to track whether the user has drafted any input.
|
||||
/// </remarks>
|
||||
private string UserInput
|
||||
{
|
||||
get => this.ComposerState.UserInput;
|
||||
set => this.ComposerState.SetUserInput(value);
|
||||
}
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Apply the filters for the message bus:
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]);
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED ]);
|
||||
|
||||
// Configure the spellchecking for the user input:
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
@@ -96,15 +118,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
// Get the preselected chat template:
|
||||
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
if (!this.ComposerState.HasUserDraft && !this.ComposerState.HasComposerContent)
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
|
||||
var deferredInput = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CHAT_INPUT).FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(deferredInput))
|
||||
this.userInput = deferredInput;
|
||||
|
||||
// Apply template's file attachments, if any:
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.SetUserInput(deferredInput);
|
||||
|
||||
//
|
||||
// Check for deferred messages of the kind 'SEND_TO_CHAT',
|
||||
@@ -122,6 +141,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.ChatThread.IncludeDateTime = true;
|
||||
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread.ChatId}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
// We know already that the chat thread is not null,
|
||||
@@ -222,6 +242,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
// Select the correct provider:
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
await this.SyncForegroundChatAsync();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
@@ -247,6 +268,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
if(this.ChatThread is not null)
|
||||
{
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
|
||||
|
||||
@@ -277,12 +299,35 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
await this.ApplyLoadedChatParameterAsync();
|
||||
await this.SyncForegroundChatAsync();
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task ApplyLoadedChatParameterAsync()
|
||||
{
|
||||
var chatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
var workspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
|
||||
|
||||
if (this.loadedParameterChatId == chatId && this.loadedParameterWorkspaceId == workspaceId)
|
||||
{
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedParameterChatId = chatId;
|
||||
this.loadedParameterWorkspaceId = workspaceId;
|
||||
await this.LoadedChatChanged(notifyParent: false);
|
||||
}
|
||||
|
||||
private void MarkCurrentChatAsLoadedParameter()
|
||||
{
|
||||
this.loadedParameterChatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
this.loadedParameterWorkspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
|
||||
}
|
||||
|
||||
private async Task SyncWorkspaceHeaderWithChatThreadAsync()
|
||||
{
|
||||
var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion);
|
||||
@@ -338,7 +383,23 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
private async Task SyncForegroundChatAsync()
|
||||
{
|
||||
var nextForegroundChatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
if (this.foregroundChatId == nextForegroundChatId)
|
||||
return;
|
||||
|
||||
if (this.foregroundChatId != Guid.Empty)
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, false);
|
||||
|
||||
this.foregroundChatId = nextForegroundChatId;
|
||||
if (this.foregroundChatId != Guid.Empty)
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, true);
|
||||
}
|
||||
|
||||
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
|
||||
|
||||
private bool IsCurrentChatStreaming => this.ChatThread is not null && this.AIJobService.IsChatGenerationActive(this.ChatThread.ChatId);
|
||||
|
||||
private string ProviderPlaceholder => this.IsProviderSelected ? T("Type your input here...") : T("Select a provider first");
|
||||
|
||||
@@ -408,12 +469,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
{
|
||||
this.currentChatTemplate = chatTemplate;
|
||||
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt);
|
||||
|
||||
// Apply template's file attachments (replaces existing):
|
||||
this.chatDocumentPaths.Clear();
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments);
|
||||
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
@@ -458,7 +517,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (!this.IsProviderSelected)
|
||||
return true;
|
||||
|
||||
if(this.isStreaming)
|
||||
if(this.IsCurrentChatStreaming)
|
||||
return true;
|
||||
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
@@ -473,6 +532,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.dataSourceSelectionComponent.Hide();
|
||||
|
||||
this.hasUnsavedChanges = true;
|
||||
this.ComposerState.MarkUserDraft();
|
||||
var key = keyEvent.Code.ToLowerInvariant();
|
||||
|
||||
// Was the enter key (either enter or numpad enter) pressed?
|
||||
@@ -507,7 +567,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
|
||||
this.dataSourceSelectionComponent.Hide();
|
||||
|
||||
this.userInput = await this.JsRuntime.InvokeAsync<string>("formatChatInputMarkdown", CHAT_INPUT_ID, formatType);
|
||||
this.ComposerState.SetUserInput(await this.JsRuntime.InvokeAsync<string>("formatChatInputMarkdown", CHAT_INPUT_ID, formatType));
|
||||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
private void ComposerAttachmentsChanged(HashSet<FileAttachment> attachments)
|
||||
{
|
||||
if (!ReferenceEquals(this.ComposerState.FileAttachments, attachments))
|
||||
this.ComposerState.ReplaceFileAttachments(attachments);
|
||||
|
||||
this.ComposerState.MarkUserDraft();
|
||||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
@@ -535,17 +604,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
DataSourceOptions = this.earlyDataSourceOptions,
|
||||
Name = this.ExtractThreadName(this.userInput),
|
||||
Name = this.ExtractThreadName(this.ComposerState.UserInput),
|
||||
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
|
||||
};
|
||||
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the thread name if it is empty:
|
||||
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
||||
this.ChatThread.Name = this.ExtractThreadName(this.userInput);
|
||||
this.ChatThread.Name = this.ExtractThreadName(this.ComposerState.UserInput);
|
||||
|
||||
// Update provider, profile and chat template:
|
||||
this.ChatThread.SelectedProvider = this.Provider.Id;
|
||||
@@ -562,14 +632,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
IContent? lastUserPrompt;
|
||||
if (!reuseLastUserPrompt)
|
||||
{
|
||||
var normalizedAttachments = this.chatDocumentPaths
|
||||
var normalizedAttachments = this.ComposerState.FileAttachments
|
||||
.Select(attachment => attachment.Normalize())
|
||||
.Where(attachment => attachment.IsValid)
|
||||
.ToList();
|
||||
|
||||
lastUserPrompt = new ContentText
|
||||
{
|
||||
Text = this.userInput,
|
||||
Text = this.ComposerState.UserInput,
|
||||
FileAttachments = normalizedAttachments,
|
||||
};
|
||||
|
||||
@@ -616,14 +686,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// Clear the input field:
|
||||
await this.inputField.FocusAsync();
|
||||
|
||||
this.userInput = string.Empty;
|
||||
this.chatDocumentPaths.Clear();
|
||||
this.ComposerState.Clear();
|
||||
|
||||
await this.inputField.BlurAsync();
|
||||
this.tokenCount = "0";
|
||||
|
||||
// Enable the stream state for the chat component:
|
||||
this.isStreaming = true;
|
||||
this.hasUnsavedChanges = true;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
@@ -633,38 +701,23 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
}
|
||||
|
||||
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
|
||||
|
||||
using (this.cancellationTokenSource = new())
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
{
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
this.cancellationTokenSource = null;
|
||||
ChatThread = this.ChatThread!,
|
||||
AIText = aiText,
|
||||
LastUserPrompt = lastUserPrompt,
|
||||
ProviderSettings = this.Provider,
|
||||
IsForeground = true,
|
||||
});
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
// Disable the stream state:
|
||||
this.isStreaming = false;
|
||||
|
||||
// Update the UI:
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task CancelStreaming()
|
||||
{
|
||||
if (this.cancellationTokenSource is not null)
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
if (this.ChatThread is not null)
|
||||
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
|
||||
}
|
||||
|
||||
private async Task SaveThread()
|
||||
@@ -694,7 +747,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||
// about possible data loss:
|
||||
//
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
@@ -727,9 +780,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
//
|
||||
// Reset our state:
|
||||
//
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
|
||||
//
|
||||
// Reset the LLM provider considering the user's settings:
|
||||
@@ -786,17 +838,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
};
|
||||
}
|
||||
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
|
||||
// Apply template's file attachments:
|
||||
this.chatDocumentPaths.Clear();
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
|
||||
// Now, we have to reset the data source options as well:
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
|
||||
// Notify the parent component about the change:
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
|
||||
@@ -805,7 +854,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
||||
{
|
||||
var confirmationDialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
@@ -838,25 +887,35 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||
|
||||
this.ChatThread!.WorkspaceId = workspaceId;
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.SaveThread();
|
||||
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadedChatChanged()
|
||||
private async Task LoadedChatChanged(bool notifyParent = true)
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
this.ChatThread = this.AIJobService.TryGetLiveChatThread(this.ChatThread.ChatId) ?? this.ChatThread;
|
||||
this.loadedParameterChatId = this.ChatThread.ChatId;
|
||||
this.loadedParameterWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
if (notifyParent)
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.loadedParameterChatId = Guid.Empty;
|
||||
this.loadedParameterWorkspaceId = Guid.Empty;
|
||||
this.ClearWorkspaceHeaderState();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
}
|
||||
|
||||
@@ -872,12 +931,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
private async Task ResetState()
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
this.ClearWorkspaceHeaderState();
|
||||
|
||||
this.ChatThread = null;
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
@@ -948,7 +1008,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(lastBlockContent is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.userInput = textBlock.Text;
|
||||
this.RestoreComposerFromTextBlock(textBlock);
|
||||
this.ChatThread.Remove(block);
|
||||
this.ChatThread.Remove(lastBlockContent);
|
||||
this.hasUnsavedChanges = true;
|
||||
@@ -965,13 +1025,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (block is not ContentText textBlock)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.userInput = textBlock.Text;
|
||||
this.RestoreComposerFromTextBlock(textBlock);
|
||||
this.ChatThread.Remove(block);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void RestoreComposerFromTextBlock(ContentText textBlock)
|
||||
{
|
||||
this.ComposerState.RestoreFromTextBlock(textBlock);
|
||||
}
|
||||
|
||||
private async Task CalculateTokenCount()
|
||||
{
|
||||
@@ -1021,8 +1086,17 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
await this.SaveThread();
|
||||
break;
|
||||
|
||||
case Event.WORKSPACE_LOADED_CHAT_CHANGED:
|
||||
await this.LoadedChatChanged();
|
||||
case Event.AI_JOB_CHANGED:
|
||||
case Event.AI_JOB_FINISHED:
|
||||
case Event.CHAT_GENERATION_CHANGED:
|
||||
if (data is AIJobSnapshot { Kind: AIJobKind.CHAT_GENERATION } snapshot && this.ChatThread?.ChatId == snapshot.SubjectId)
|
||||
{
|
||||
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
|
||||
if (!snapshot.IsActive)
|
||||
this.hasUnsavedChanges = false;
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1034,8 +1108,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
case Event.HAS_CHAT_UNSAVED_CHANGES:
|
||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
if (this.IsCurrentChatStreaming)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
|
||||
return Task.FromResult((TResult?)(object)(this.hasUnsavedChanges || this.ComposerState.HasVisibleUserDraft));
|
||||
}
|
||||
|
||||
return Task.FromResult(default(TResult));
|
||||
@@ -1053,21 +1130,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
if (this.cancellationTokenSource is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
|
||||
this.cancellationTokenSource.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, false);
|
||||
this.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public sealed class ChatComposerState
|
||||
{
|
||||
public string UserInput { get; private set; } = string.Empty;
|
||||
|
||||
public HashSet<FileAttachment> FileAttachments { get; } = [];
|
||||
|
||||
public bool HasUserDraft { get; private set; }
|
||||
|
||||
public bool HasComposerContent => !string.IsNullOrWhiteSpace(this.UserInput) || this.FileAttachments.Count > 0;
|
||||
|
||||
public bool HasVisibleUserDraft => this.HasUserDraft && (!string.IsNullOrWhiteSpace(this.UserInput) || this.FileAttachments.Count > 0);
|
||||
|
||||
public void ApplyTemplate(ChatTemplate chatTemplate)
|
||||
{
|
||||
this.UserInput = chatTemplate.PredefinedUserPrompt;
|
||||
this.FileAttachments.Clear();
|
||||
foreach (var attachment in chatTemplate.FileAttachments)
|
||||
this.FileAttachments.Add(attachment.Normalize());
|
||||
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void SetUserInput(string? userInput)
|
||||
{
|
||||
this.UserInput = userInput ?? string.Empty;
|
||||
this.HasUserDraft = !string.IsNullOrWhiteSpace(userInput);
|
||||
}
|
||||
|
||||
public void SetSystemInput(string? userInput)
|
||||
{
|
||||
this.UserInput = userInput ?? string.Empty;
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void MarkUserDraft()
|
||||
{
|
||||
this.HasUserDraft = true;
|
||||
}
|
||||
|
||||
public void ReplaceFileAttachments(IEnumerable<FileAttachment> fileAttachments)
|
||||
{
|
||||
this.FileAttachments.Clear();
|
||||
foreach (var attachment in fileAttachments)
|
||||
this.FileAttachments.Add(attachment.Normalize());
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
this.UserInput = string.Empty;
|
||||
this.FileAttachments.Clear();
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void RestoreFromTextBlock(ContentText textBlock)
|
||||
{
|
||||
this.UserInput = textBlock.Text;
|
||||
this.ReplaceFileAttachments(textBlock.FileAttachments);
|
||||
this.HasUserDraft = true;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Color theme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="@T("Choose the color theme that best suits for you.")"/>
|
||||
<ConfigurationOption OptionDescription="@T("Save energy?")" LabelOn="@T("Energy saving is enabled")" LabelOff="@T("Energy saving is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="@T("When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available.")"/>
|
||||
<ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/>
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Request timeout")" Min="@ExternalHttpClientTimeout.MIN_HTTP_CLIENT_TIMEOUT_SECONDS" Max="@ExternalHttpClientTimeout.MAX_HTTP_CLIENT_TIMEOUT_SECONDS" Step="60" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds = updatedValue)" OptionHelp="@T("How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.HttpClientTimeoutSeconds, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInterval)" Data="@ConfigurationSelectDataFactory.GetUpdateIntervalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@T("How often should we check for app updates?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@T("Should updates be installed automatically or manually?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.VoiceChat" HeaderText="@T("Configure Transcription Providers")">
|
||||
<PreviewBeta ApplyInnerScrollingFix="true"/>
|
||||
<MudText Typo="Typo.h4" Class="mb-3">
|
||||
@T("Configured Transcription Providers")
|
||||
</MudText>
|
||||
|
||||
@@ -12,10 +12,16 @@ public class TreeItemData : ITreeItem
|
||||
|
||||
public string Icon { get; init; } = string.Empty;
|
||||
|
||||
public string DefaultIcon { get; init; } = string.Empty;
|
||||
|
||||
public TreeItemType Type { get; init; }
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public Guid ChatId { get; init; }
|
||||
|
||||
public Guid WorkspaceId { get; init; }
|
||||
|
||||
public bool Expandable { get; init; } = true;
|
||||
|
||||
public DateTimeOffset LastEditTime { get; init; }
|
||||
|
||||
@@ -132,6 +132,7 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
}
|
||||
|
||||
var mimeTypes = GetPreferredMimeTypes(
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.WEBM).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.OGG).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.AAC).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP3).Build(),
|
||||
@@ -361,7 +362,18 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
|
||||
// Call the transcription API:
|
||||
this.Logger.LogInformation("Starting transcription with provider '{ProviderName}' and model '{ModelName}'.", transcriptionProviderSettings.UsedLLMProvider, transcriptionProviderSettings.Model.ToString());
|
||||
var transcribedText = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager);
|
||||
var transcriptionResult = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager);
|
||||
if (!transcriptionResult.Success)
|
||||
{
|
||||
this.Logger.LogWarning("The transcription request failed.");
|
||||
var userMessage = string.IsNullOrWhiteSpace(transcriptionResult.ErrorMessage)
|
||||
? this.T("Unfortunately, there was an error communicating with the AI system.")
|
||||
: transcriptionResult.ErrorMessage;
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, userMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
var transcribedText = transcriptionResult.Text;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transcribedText))
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ else
|
||||
case TreeItemData treeItem:
|
||||
@if (treeItem.Type is TreeItemType.LOADING)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@(treeItem.Children!)">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@(treeItem.Children!)">
|
||||
<BodyContent>
|
||||
<MudSkeleton Width="85%" Height="22px"/>
|
||||
</BodyContent>
|
||||
@@ -32,7 +32,7 @@ else
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.CHAT)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@@ -48,15 +48,15 @@ else
|
||||
<div style="justify-self: end;">
|
||||
|
||||
<MudTooltip Text="@T("Move to workspace")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@ else
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.WORKSPACE)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.OnWorkspaceClicked(treeItem))">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.OnWorkspaceClicked(treeItem))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@@ -86,7 +86,7 @@ else
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -18,6 +19,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
[Inject]
|
||||
private ILogger<Workspaces> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AIJobService AIJobService { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public ChatThread? CurrentChatThread { get; set; }
|
||||
@@ -42,6 +46,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
this.ApplyFilters([], [ Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED ]);
|
||||
_ = this.LoadTreeItemsAsync(startPrefetch: true);
|
||||
}
|
||||
|
||||
@@ -111,7 +116,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (var temporaryChat in snapshot.TemporaryChats.OrderByDescending(x => x.LastEditTime))
|
||||
temporaryChatsChildren.Add(CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
|
||||
temporaryChatsChildren.Add(this.CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
@@ -136,7 +141,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (workspace.ChatsLoaded)
|
||||
{
|
||||
foreach (var workspaceChat in workspace.Chats.OrderByDescending(x => x.LastEditTime))
|
||||
children.Add(CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
|
||||
children.Add(this.CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
|
||||
}
|
||||
else if (this.loadingWorkspaceChatLists.Contains(workspace.WorkspaceId))
|
||||
children.AddRange(this.CreateLoadingRows(workspace.WorkspacePath));
|
||||
@@ -192,7 +197,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
};
|
||||
}
|
||||
|
||||
private static TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon)
|
||||
private TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon)
|
||||
{
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
@@ -204,13 +209,44 @@ public partial class Workspaces : MSGComponentBase
|
||||
Branch = branch,
|
||||
Text = chat.Name,
|
||||
Icon = icon,
|
||||
DefaultIcon = icon,
|
||||
Expandable = false,
|
||||
Path = chat.ChatPath,
|
||||
ChatId = chat.ChatId,
|
||||
WorkspaceId = chat.WorkspaceId,
|
||||
LastEditTime = chat.LastEditTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private string GetTreeItemIcon(TreeItemData treeItem)
|
||||
{
|
||||
if (treeItem.Type is not TreeItemType.CHAT)
|
||||
return treeItem.Icon;
|
||||
|
||||
var defaultIcon = string.IsNullOrWhiteSpace(treeItem.DefaultIcon) ? treeItem.Icon : treeItem.DefaultIcon;
|
||||
return this.GetChatTreeIcon(treeItem.ChatId, defaultIcon);
|
||||
}
|
||||
|
||||
private bool IsChatTreeItemBusy(TreeItemData treeItem)
|
||||
{
|
||||
return treeItem.Type is TreeItemType.CHAT && this.AIJobService.IsChatGenerationActive(treeItem.ChatId);
|
||||
}
|
||||
|
||||
private string GetChatTreeIcon(Guid chatId, string defaultIcon)
|
||||
{
|
||||
var snapshot = this.AIJobService.TryGetChatSnapshot(chatId);
|
||||
if (snapshot is null || !snapshot.IsActive)
|
||||
return defaultIcon;
|
||||
|
||||
return snapshot.Status switch
|
||||
{
|
||||
AIJobStatus.WAITING_FOR_REMOTE => Icons.Material.Filled.HourglassTop,
|
||||
AIJobStatus.RUNNING => Icons.Material.Filled.ChangeCircle,
|
||||
_ => defaultIcon,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SafeStateHasChanged()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
@@ -348,11 +384,13 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
|
||||
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, WorkspaceBehaviour.JSON_OPTIONS);
|
||||
if (chat is not null)
|
||||
chat = this.AIJobService.TryGetLiveChatThread(chat.ChatId) ?? chat;
|
||||
|
||||
if (switchToChat)
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
return chat;
|
||||
@@ -371,6 +409,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
if (askForConfirmation)
|
||||
{
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId);
|
||||
@@ -398,7 +439,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread = null;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +447,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<SingleInputDialog>
|
||||
{
|
||||
@@ -429,7 +472,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread.Name = chat.Name;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
@@ -525,6 +567,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<WorkspaceSelectionDialog>
|
||||
{
|
||||
@@ -549,7 +594,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
@@ -597,6 +641,12 @@ public partial class Workspaces : MSGComponentBase
|
||||
case Event.PLUGINS_RELOADED:
|
||||
await this.ForceRefreshFromDiskAsync();
|
||||
break;
|
||||
|
||||
case Event.AI_JOB_CHANGED:
|
||||
case Event.AI_JOB_FINISHED:
|
||||
case Event.CHAT_GENERATION_CHANGED:
|
||||
await this.SafeStateHasChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user