mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 22:07:21 +00:00
Fixed chats being stored when chats are stored manually or workspaces are disabled (#1145)
This commit is contained in:
1 parent
9532664a25
commit
64dc621a7d
12 files changed
+415
-71
No files matched your search
@@ -4024,9 +4024,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1028969501"] = "{0}, plus
|
||||
-- Move chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Move chat"
|
||||
|
||||
-- Are you sure you want to move this chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Are you sure you want to move this chat? All unsaved changes will be lost."
|
||||
|
||||
-- Bold
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Bold"
|
||||
|
||||
@@ -4063,9 +4060,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2059172343"] = "plus {0}
|
||||
-- Are you sure you want to start a new chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2111282488"] = "Are you sure you want to start a new chat? All unsaved changes will be lost."
|
||||
|
||||
-- Unsaved Changes
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2123670756"] = "Unsaved Changes"
|
||||
|
||||
-- Start New Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2310454789"] = "Start New Chat"
|
||||
|
||||
|
||||
@@ -674,6 +674,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
}
|
||||
|
||||
var leftChatHadNoThread = !isFirstChat && this.loadedParameterChatId == Guid.Empty;
|
||||
if (!isFirstChat)
|
||||
await this.StopAnswerOfLeftChatAsync(this.loadedParameterChatId);
|
||||
|
||||
this.loadedParameterChatId = chatId;
|
||||
this.loadedParameterWorkspaceId = workspaceId;
|
||||
|
||||
@@ -1580,9 +1583,15 @@ public partial class ChatComponent : MSGComponentBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and stores the chat the composer belongs to, when there is none yet, e.g., for what an
|
||||
/// attachment brings along before the first message is sent.
|
||||
/// Creates the chat the composer belongs to, when there is none yet, e.g., for what an attachment
|
||||
/// brings along before the first message is sent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only while chats are stored automatically, the new chat reaches the disk right away. Otherwise, it
|
||||
/// counts as unsaved, like a message typed into a new chat: a transcript made for it waits in the
|
||||
/// staging area until the user saves the chat, see WorkspaceBehaviour.StoreChatAsync, and the next
|
||||
/// start removes it when they never do.
|
||||
/// </remarks>
|
||||
/// <param name="nameWithoutInput">The name of the chat while the user typed nothing yet.</param>
|
||||
/// <returns>The chat of the composer.</returns>
|
||||
private async Task<ChatThread?> EnsureChatAsync(string nameWithoutInput)
|
||||
@@ -1615,7 +1624,11 @@ public partial class ChatComponent : MSGComponentBase
|
||||
// lists the new chat right away, together with the progress of the transcription, instead
|
||||
// of only after the first message was sent:
|
||||
//
|
||||
await this.StoreAndListChatAsync(chat);
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
await this.StoreAndListChatAsync(chat);
|
||||
else
|
||||
this.hasUnsavedChanges = true;
|
||||
|
||||
await this.SyncForegroundChatAsync();
|
||||
return chat;
|
||||
}
|
||||
@@ -1766,7 +1779,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
|
||||
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, this.ChatThread.RequiredOutboundDataRestriction.Restriction);
|
||||
|
||||
// From here on, the AI job stores the chat, in every storage mode:
|
||||
// From here on, the AI job stores the chat, while chats are stored automatically:
|
||||
this.unstoredChat = null;
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
{
|
||||
@@ -1786,6 +1799,28 @@ public partial class ChatComponent : MSGComponentBase
|
||||
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the answer which is still being written in a chat the user leaves, unless chats are stored automatically.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only while chats are stored automatically, the AI job stores the answer, so that the user finds
|
||||
/// it when coming back. Otherwise, the answer would end up nowhere, and writing it would only cost
|
||||
/// the user. The user was asked before, since an answer being written counts as an unsaved change
|
||||
/// then.<br/><br/>
|
||||
///
|
||||
/// Called only where the user leaves a chat: opening another one, starting a new one, or leaving the
|
||||
/// chat page. Never when this component goes away, since the page creates it anew, e.g., whenever the
|
||||
/// workspace sidebar is shown or hidden, while the chat stays.
|
||||
/// </remarks>
|
||||
/// <param name="chatId">The chat being left.</param>
|
||||
private async Task StopAnswerOfLeftChatAsync(Guid chatId)
|
||||
{
|
||||
if (chatId == Guid.Empty || this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return;
|
||||
|
||||
await this.AIJobService.CancelChatGenerationAsync(chatId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes over the tool selection of the chat that was just loaded or handed to this component.
|
||||
/// </summary>
|
||||
@@ -1825,7 +1860,21 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if (!this.CanThreadBeSaved)
|
||||
return;
|
||||
|
||||
var stagedTranscripts = this.ChatThread.Draft.MediaTranscripts.Where(transcript => transcript.IsStaged).Select(transcript => transcript.FilePath).ToHashSet(StringComparer.Ordinal);
|
||||
await this.StoreAndListChatAsync(this.ChatThread);
|
||||
|
||||
//
|
||||
// Storing moved the staged transcripts of the message being written into the chat directory,
|
||||
// see WorkspaceBehaviour.StoreChatAsync. The composer still knows them by where they were, so
|
||||
// sending the message would lose them:
|
||||
//
|
||||
if (stagedTranscripts.Count > 0)
|
||||
{
|
||||
this.ComposerState.FileAttachments.RemoveWhere(attachment => stagedTranscripts.Contains(attachment.FilePath));
|
||||
foreach (var transcript in this.ChatThread.Draft.MediaTranscripts)
|
||||
this.ComposerState.FileAttachments.Add(transcript);
|
||||
}
|
||||
|
||||
this.hasUnsavedChanges = false;
|
||||
|
||||
// A new chat is on the disk now, like any other:
|
||||
@@ -1876,9 +1925,11 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
//
|
||||
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||
// about possible data loss:
|
||||
// about possible data loss. An answer still being written is lost as well, whenever chats are
|
||||
// not stored automatically, since leaving stops it, see StopAnswerOfLeftChatAsync:
|
||||
//
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
||||
var storageBehavior = this.SettingsManager.ConfigurationData.Workspace.StorageBehavior;
|
||||
if ((storageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges) || (storageBehavior is not WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY && this.IsCurrentChatStreaming))
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
@@ -1930,6 +1981,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.ChatThread is not null)
|
||||
await this.StopAnswerOfLeftChatAsync(this.ChatThread.ChatId);
|
||||
|
||||
//
|
||||
// Reset our state:
|
||||
//
|
||||
@@ -2034,19 +2088,6 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
||||
{
|
||||
var confirmationDialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, T("Are you sure you want to move this chat? All unsaved changes will be lost.") },
|
||||
};
|
||||
|
||||
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Unsaved Changes"), confirmationDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var confirmationDialogResult = await confirmationDialogReference.Result;
|
||||
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
var dialogParameters = new DialogParameters<WorkspaceSelectionDialog>
|
||||
{
|
||||
{ x => x.Message, T("Please select the workspace where you want to move the chat to.") },
|
||||
@@ -2065,13 +2106,15 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
//
|
||||
// Like saving and copying, the move goes through the workspace component whenever there is
|
||||
// one, so its tree shows the chat in the new place right away:
|
||||
// one, so its tree shows the chat in the new place right away. Nothing the user did not save
|
||||
// gets lost, so nothing is asked: it reaches the new place only while chats are stored
|
||||
// automatically, and otherwise stays unsaved, see WorkspaceBehaviour.MoveChatAsync:
|
||||
//
|
||||
var previousWorkspaceId = this.ChatThread!.WorkspaceId;
|
||||
if (this.Workspaces is not null)
|
||||
await this.Workspaces.MoveChatAsync(this.ChatThread!, workspaceId);
|
||||
else
|
||||
await WorkspaceBehaviour.MoveChatAsync(this.ChatThread!, workspaceId);
|
||||
await WorkspaceBehaviour.MoveChatAsync(this.ChatThread!, workspaceId, this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY);
|
||||
|
||||
await this.FollowMovedChatAsync(previousWorkspaceId);
|
||||
}
|
||||
@@ -2204,6 +2247,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{
|
||||
await this.FlushDraftAsync();
|
||||
await this.PreserveNewChatDraftAsync(this.ChatThread is null);
|
||||
if (this.ChatThread is not null)
|
||||
await this.StopAnswerOfLeftChatAsync(this.ChatThread.ChatId);
|
||||
|
||||
this.hasUnsavedChanges = false;
|
||||
this.ComposerState.Clear();
|
||||
this.ClearWorkspaceHeaderState();
|
||||
@@ -2283,8 +2329,11 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if (!this.blockActions.TryRemove(this.ChatThread, block))
|
||||
return;
|
||||
|
||||
// Like every other change, stored right away only while chats are stored automatically:
|
||||
this.hasUnsavedChanges = true;
|
||||
await this.SaveThread();
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
await this.SaveThread();
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -2310,8 +2359,12 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
// The rollback reset the AI-selected data sources, which the data source selection still shows:
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||
|
||||
// Like every other change, stored right away only while chats are stored automatically:
|
||||
this.hasUnsavedChanges = true;
|
||||
await this.SaveThread();
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
await this.SaveThread();
|
||||
|
||||
this.tokenTracker?.Nudge();
|
||||
this.StateHasChanged();
|
||||
await this.inputField.FocusAsync();
|
||||
@@ -2568,7 +2621,8 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
|
||||
if (!snapshot.IsActive)
|
||||
{
|
||||
this.hasUnsavedChanges = false;
|
||||
// The job stored the answer only while chats are stored automatically, see AIJobService.CheckpointChatAsync:
|
||||
this.hasUnsavedChanges = this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY;
|
||||
this.previousInputForbidden = true;
|
||||
}
|
||||
|
||||
@@ -2587,8 +2641,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) false));
|
||||
|
||||
// Nothing stores an answer being written now, and leaving stops it, see StopAnswerOfLeftChatAsync:
|
||||
if (this.IsCurrentChatStreaming)
|
||||
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) false));
|
||||
return Task.FromResult(new MessageBusAnswer<TResult>(true, (TResult) (object) true));
|
||||
|
||||
//
|
||||
// A message the user started counts as unsaved on its own only while there is no chat
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -1009,11 +1010,14 @@ public partial class Workspaces : MSGComponentBase
|
||||
/// Neither asks for the target workspace nor follows the chat in the chat view: both depend on
|
||||
/// where the move was asked for. Whoever moves a chat while this component exists goes through
|
||||
/// here, though, since the tree draws what it has built before and would otherwise keep showing
|
||||
/// the chat in its old place.
|
||||
/// the chat in its old place.<br/><br/>
|
||||
///
|
||||
/// What the user did not save reaches the new place only while chats are stored automatically,
|
||||
/// see WorkspaceBehaviour.MoveChatAsync.
|
||||
/// </remarks>
|
||||
public async Task MoveChatAsync(ChatThread chat, Guid targetWorkspaceId)
|
||||
{
|
||||
await WorkspaceBehaviour.MoveChatAsync(chat, targetWorkspaceId);
|
||||
await WorkspaceBehaviour.MoveChatAsync(chat, targetWorkspaceId, this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY);
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
@@ -1024,9 +1028,10 @@ public partial class Workspaces : MSGComponentBase
|
||||
return;
|
||||
|
||||
//
|
||||
// The open chat moves as it stands on the screen, just like with the button in the chat
|
||||
// itself. The copy from the disk lacks whatever is not stored yet, such as a message being
|
||||
// edited, and the chat would continue with that copy afterward:
|
||||
// The open chat is the one which moves, just like with the button in the chat itself. The
|
||||
// copy from the disk lacks whatever is not stored yet, such as a message being edited, and
|
||||
// the chat would continue with that copy afterward. Whether that reaches the disk in the new
|
||||
// place depends on how chats are stored, see MoveChatAsync:
|
||||
//
|
||||
var chat = this.CurrentChatThread is { } openChat && openChat.ChatId == storedChat.ChatId ? openChat : storedChat;
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
|
||||
-6
@@ -4026,9 +4026,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1028969501"] = "{0}, plus
|
||||
-- Move chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Chat verschieben"
|
||||
|
||||
-- Are you sure you want to move this chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Sind Sie sicher, dass Sie diesen Chat verschieben möchten? Alle ungespeicherten Änderungen gehen verloren."
|
||||
|
||||
-- Bold
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Fett"
|
||||
|
||||
@@ -4065,9 +4062,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2059172343"] = "plus {0}
|
||||
-- Are you sure you want to start a new chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2111282488"] = "Möchten Sie wirklich einen neuen Chat starten? Alle nicht gespeicherten Änderungen gehen verloren."
|
||||
|
||||
-- Unsaved Changes
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2123670756"] = "Nicht gespeicherte Änderungen"
|
||||
|
||||
-- Start New Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2310454789"] = "Neuen Chat starten"
|
||||
|
||||
|
||||
-6
@@ -4026,9 +4026,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1028969501"] = "{0}, plus
|
||||
-- Move chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1133040906"] = "Move chat"
|
||||
|
||||
-- Are you sure you want to move this chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1142475422"] = "Are you sure you want to move this chat? All unsaved changes will be lost."
|
||||
|
||||
-- Bold
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T1165397398"] = "Bold"
|
||||
|
||||
@@ -4065,9 +4062,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2059172343"] = "plus {0}
|
||||
-- Are you sure you want to start a new chat? All unsaved changes will be lost.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2111282488"] = "Are you sure you want to start a new chat? All unsaved changes will be lost."
|
||||
|
||||
-- Unsaved Changes
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2123670756"] = "Unsaved Changes"
|
||||
|
||||
-- Start New Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CHATCOMPONENT::T2310454789"] = "Start New Chat"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.RAG.RAGProcesses;
|
||||
|
||||
@@ -175,7 +176,7 @@ public sealed class AIJobService(SettingsManager settingsManager, MessageBus mes
|
||||
|
||||
request.AIText.InitialRemoteWait = true;
|
||||
request.AIText.IsStreaming = false;
|
||||
await CheckpointChatAsync(state, force: true);
|
||||
await this.CheckpointChatAsync(state, force: true);
|
||||
await this.NotifyChangedAsync(state);
|
||||
|
||||
//
|
||||
@@ -329,7 +330,7 @@ public sealed class AIJobService(SettingsManager settingsManager, MessageBus mes
|
||||
await this.NotifyChangedAsync(state);
|
||||
}
|
||||
|
||||
await CheckpointChatAsync(state);
|
||||
await this.CheckpointChatAsync(state);
|
||||
}
|
||||
|
||||
await this.CompleteChatGenerationAsync(state, token.IsCancellationRequested ? AIJobStatus.CANCELED : AIJobStatus.COMPLETED);
|
||||
@@ -393,7 +394,7 @@ public sealed class AIJobService(SettingsManager settingsManager, MessageBus mes
|
||||
}
|
||||
|
||||
this.activeChatJobsByChatId.TryRemove(request.ChatThread.ChatId, out _);
|
||||
await CheckpointChatAsync(state, force: true);
|
||||
await this.CheckpointChatAsync(state, force: true);
|
||||
await this.NotifyChangedAsync(state);
|
||||
await messageBus.SendMessage(null, Event.AI_JOB_FINISHED, state.Snapshot);
|
||||
state.CancellationTokenSource.Dispose();
|
||||
@@ -505,8 +506,26 @@ public sealed class AIJobService(SettingsManager settingsManager, MessageBus mes
|
||||
await messageBus.SendMessage(null, Event.AI_JOB_CHANGED, state.Snapshot);
|
||||
}
|
||||
|
||||
private static async Task CheckpointChatAsync(AIJobState state, bool force = false)
|
||||
/// <summary>
|
||||
/// Stores the chat of a job, at most once per <see cref="CHECKPOINT_MIN_TIME"/> unless forced.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only while chats are stored automatically. Whoever stores chats manually decides when a chat
|
||||
/// reaches the disk, with its save button, and whoever disabled the workspaces wants no chat on the
|
||||
/// disk at all. Storing it here anyway would put every message on the disk the moment it is sent,
|
||||
/// and overwrite what the user saved by hand with what they did not save. The chat marks the answer
|
||||
/// as unsaved instead, once the job is done.<br/><br/>
|
||||
///
|
||||
/// The storage mode is read every time rather than once for the job: whoever changes it while an
|
||||
/// answer is being written gets what the chat reads when the answer is done.
|
||||
/// </remarks>
|
||||
/// <param name="state">The job whose chat to store.</param>
|
||||
/// <param name="force">True stores the chat even when the last checkpoint was just now.</param>
|
||||
private async Task CheckpointChatAsync(AIJobState state, bool force = false)
|
||||
{
|
||||
if (settingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return;
|
||||
|
||||
var now = DateTimeOffset.Now;
|
||||
if (!force && now - state.LastCheckpoint < CHECKPOINT_MIN_TIME)
|
||||
return;
|
||||
|
||||
@@ -314,9 +314,15 @@ public sealed class MediaTranscriptionService(
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
// Only while chats are stored automatically, the transcript goes right into a stored chat,
|
||||
// which is stored along with it. Otherwise, storing would put whatever the user did not save
|
||||
// on the disk, so the transcript waits in the staging area until the user saves the chat:
|
||||
//
|
||||
var persistentStorage = transcriptStorages.FirstOrDefault(storage => storage.CanStore(target.Owner));
|
||||
var isPersistedChat = persistentStorage is null &&
|
||||
ownerChat is not null &&
|
||||
settingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY &&
|
||||
WorkspaceBehaviour.IsChatExisting(new LoadChat(ownerChat.WorkspaceId, ownerChat.ChatId));
|
||||
|
||||
var attachment = persistentStorage is not null
|
||||
|
||||
@@ -1116,7 +1116,19 @@ public static partial class WorkspaceBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task MoveChatAsync(ChatThread chat, Guid targetWorkspaceId)
|
||||
/// <summary>
|
||||
/// Moves a chat, together with its directory, into another workspace.
|
||||
/// </summary>
|
||||
/// <param name="chat">The chat to move, as it stands in memory. It gets the new workspace and the new paths of its attachments.</param>
|
||||
/// <param name="targetWorkspaceId">The workspace to move the chat into.</param>
|
||||
/// <param name="storeUnsavedChanges">True stores the chat as it stands in memory in its new place, the way chats are stored automatically. False moves the chat as it was stored last, and the chat in memory only follows.</param>
|
||||
/// <remarks>
|
||||
/// Whoever stores chats manually decides with the save button what reaches the disk. Moving such a
|
||||
/// chat therefore must neither store what the user did not save nor throw it away: the stored chat
|
||||
/// moves, and what the user did not save stays in memory, unsaved as before, until they save it in
|
||||
/// its new place.
|
||||
/// </remarks>
|
||||
public static async Task MoveChatAsync(ChatThread chat, Guid targetWorkspaceId, bool storeUnsavedChanges)
|
||||
{
|
||||
if (chat.WorkspaceId == targetWorkspaceId)
|
||||
return;
|
||||
@@ -1136,6 +1148,9 @@ public static partial class WorkspaceBehaviour
|
||||
await orderedSemaphores[1].WaitAsync();
|
||||
|
||||
var moved = false;
|
||||
|
||||
// What is written in the new place: the chat in memory, or the one stored last.
|
||||
ChatThread? movedChat = null;
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(sourceDirectory))
|
||||
@@ -1151,14 +1166,22 @@ public static partial class WorkspaceBehaviour
|
||||
Directory.Move(sourceDirectory, targetDirectory);
|
||||
moved = true;
|
||||
|
||||
UpdateAttachmentPathsAfterMove(chat, sourceDirectory, targetDirectory);
|
||||
chat.WorkspaceId = targetWorkspaceId;
|
||||
movedChat = storeUnsavedChanges ? chat : await ReadMovedChatAsync(targetDirectory);
|
||||
UpdateAttachmentPathsAfterMove(movedChat, sourceDirectory, targetDirectory);
|
||||
movedChat.WorkspaceId = targetWorkspaceId;
|
||||
|
||||
FinalizeStagedTranscripts(chat, targetDirectory);
|
||||
await StoreMovedChatFilesAsync(chat, targetDirectory);
|
||||
FinalizeStagedTranscripts(movedChat, targetDirectory);
|
||||
await StoreMovedChatFilesAsync(movedChat, targetDirectory);
|
||||
|
||||
var lastEditTime = File.GetLastWriteTimeUtc(Path.Join(targetDirectory, "thread.json"));
|
||||
await UpdateCacheAfterChatMoved(sourceWorkspaceId, targetWorkspaceId, chat.ChatId, targetDirectory, chat.Name, lastEditTime);
|
||||
await UpdateCacheAfterChatMoved(sourceWorkspaceId, targetWorkspaceId, chat.ChatId, targetDirectory, movedChat.Name, lastEditTime);
|
||||
|
||||
// The chat in memory follows, with whatever the user did not save, which stays unsaved:
|
||||
if (!ReferenceEquals(movedChat, chat))
|
||||
{
|
||||
UpdateAttachmentPathsAfterMove(chat, sourceDirectory, targetDirectory);
|
||||
chat.WorkspaceId = targetWorkspaceId;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -1166,14 +1189,17 @@ public static partial class WorkspaceBehaviour
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateAttachmentPathsAfterMove(chat, targetDirectory, sourceDirectory);
|
||||
chat.WorkspaceId = sourceWorkspaceId;
|
||||
if (movedChat is not null)
|
||||
{
|
||||
UpdateAttachmentPathsAfterMove(movedChat, targetDirectory, sourceDirectory);
|
||||
movedChat.WorkspaceId = sourceWorkspaceId;
|
||||
}
|
||||
|
||||
if (Directory.Exists(targetDirectory) && !Directory.Exists(sourceDirectory))
|
||||
Directory.Move(targetDirectory, sourceDirectory);
|
||||
|
||||
if (Directory.Exists(sourceDirectory))
|
||||
await StoreMovedChatFilesAsync(chat, sourceDirectory);
|
||||
if (movedChat is not null && Directory.Exists(sourceDirectory))
|
||||
await StoreMovedChatFilesAsync(movedChat, sourceDirectory);
|
||||
}
|
||||
catch (Exception rollbackError)
|
||||
{
|
||||
@@ -1192,6 +1218,14 @@ public static partial class WorkspaceBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads the chat as it was stored last, from the directory it was just moved to.</summary>
|
||||
/// <remarks>Its paths still point to where the chat was before the move.</remarks>
|
||||
private static async Task<ChatThread> ReadMovedChatAsync(string chatDirectory)
|
||||
{
|
||||
var chatData = await File.ReadAllTextAsync(Path.Join(chatDirectory, "thread.json"), Encoding.UTF8);
|
||||
return JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS) ?? throw new InvalidDataException($"The stored chat could not be read: '{chatDirectory}'.");
|
||||
}
|
||||
|
||||
/// <summary>Atomically stores the name and thread after a directory move.</summary>
|
||||
private static async Task StoreMovedChatFilesAsync(ChatThread chat, string chatDirectory)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- Fixed the ERI Server assistant not writing some generated files to disk, such as docker-compose.yml. It now tells you about every file it could not write.
|
||||
- Fixed configuration plugins being unable to lock the preselected provider, the preselected profile, or the transcription provider to none. An empty value now does this, as the example configuration plugin describes. Older versions of AI Studio ignore the empty value and leave the choice to your users.
|
||||
- Fixed AI Studio loading none of your organization's configurations when one place, such as the Windows registry, named a configuration with an invalid ID. AI Studio now skips that entry and uses the configurations from the other places.
|
||||
- Fixed chats being stored although you chose "Store chats manually" or "Disable workspaces". AI Studio now stores a chat only when you click "Save chat", or not at all while workspaces are disabled. In both modes, leaving a chat whose answer is still being written asks you first and stops that answer. Chats outside of workspaces that earlier versions stored this way appear among your disappearing chats once you use workspaces again.
|
||||
- Upgraded several libraries to improve security.
|
||||
- Upgraded Tauri to v2.12.5.
|
||||
- Upgraded MudBlazor to v9.12.0.
|
||||
Reference in new issue
Block a user