mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 02:13:48 +00:00
Kept media transcripts when editing a message or moving a chat (#1047)
This commit is contained in:
1 parent
3a1f586f76
commit
5ee4dfa2b5
8 files changed
+461
-28
No files matched your search
@@ -469,6 +469,39 @@ public sealed record ChatThread
|
||||
this.Blocks.Remove(block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a user message whose text and attachments went back into the composer for editing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike Remove, this keeps the files of the managed transcripts. The composer still carries
|
||||
/// them, so deleting the files would leave the edited message pointing at nothing, and the user
|
||||
/// would have to transcribe the media all over again.<br/><br/>
|
||||
///
|
||||
/// A transcript always belongs to exactly one owner: to a sent message, or to the draft in the
|
||||
/// composer, which this thread keeps in PendingMediaTranscripts. Sending hands the transcripts of
|
||||
/// the draft to the new message; editing is the way back. From then on, the rules of the draft
|
||||
/// apply: removing the attachment from the composer deletes its file, and opening the chat again
|
||||
/// brings it back into the composer.
|
||||
/// </remarks>
|
||||
/// <param name="content">The content of the user message to edit.</param>
|
||||
public void RemoveForEditing(IContent content)
|
||||
{
|
||||
var block = this.Blocks.FirstOrDefault(x => x.Content == content);
|
||||
if (block is null)
|
||||
return;
|
||||
|
||||
if (block.Content is ContentText textContent)
|
||||
{
|
||||
foreach (var transcript in textContent.FileAttachments.OfType<ManagedTranscriptAttachment>())
|
||||
{
|
||||
if (this.PendingMediaTranscripts.All(existing => existing.FilePath != transcript.FilePath))
|
||||
this.PendingMediaTranscripts.Add(transcript);
|
||||
}
|
||||
}
|
||||
|
||||
this.Blocks.Remove(block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rolls this chat thread back, so that the given content becomes the last block of the conversation.
|
||||
/// </summary>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
@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 || this.IsCurrentChatStreaming)" OnClick="@this.MoveChatToWorkspace"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeMoved)" OnClick="@this.MoveChatToWorkspace"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Copy this chat & continue in the copy.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
|
||||
@@ -549,6 +549,16 @@ public partial class ChatComponent : MSGComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// The same chat in another workspace was moved there, e.g., through the workspace tree. Only
|
||||
// a move changes the workspace of a chat; every other way to open one brings another chat ID:
|
||||
//
|
||||
if (chatId != Guid.Empty && this.loadedParameterChatId == chatId)
|
||||
{
|
||||
await this.FollowMovedChatAsync(this.loadedParameterWorkspaceId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedParameterChatId = chatId;
|
||||
this.loadedParameterWorkspaceId = workspaceId;
|
||||
await this.LoadedChatChanged(notifyParent: false);
|
||||
@@ -673,6 +683,21 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private bool CanThreadBeCopied => this.CanThreadBeSaved && !this.IsCurrentChatStreaming && !this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the open chat can move into a workspace, which is exactly when the workspace tree offers it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The tree lists every stored chat, even one without a message yet, such as a chat which was
|
||||
/// stored right away to keep the transcript of an audio or video file. A chat not stored yet has
|
||||
/// no directory to move. While an answer is generated or a transcript is made, the chat stays
|
||||
/// where it is, just like in the tree. The directory check comes last, because this runs with
|
||||
/// every render.
|
||||
/// </remarks>
|
||||
private bool CanThreadBeMoved => this.ChatThread is not null
|
||||
&& !this.IsCurrentChatStreaming
|
||||
&& !this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner)
|
||||
&& WorkspaceBehaviour.IsChatExisting(new LoadChat(this.ChatThread.WorkspaceId, this.ChatThread.ChatId));
|
||||
|
||||
private string TooltipAddChatToWorkspace => string.Format(T("Start new chat in workspace '{0}'"), this.currentWorkspaceName);
|
||||
|
||||
private string UserInputStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : string.Empty;
|
||||
@@ -1366,9 +1391,35 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if (workspaceId == Guid.Empty)
|
||||
return;
|
||||
|
||||
var previousWorkspaceId = this.ChatThread!.WorkspaceId;
|
||||
await WorkspaceBehaviour.MoveChatAsync(this.ChatThread!, workspaceId);
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.FollowMovedChatAsync(previousWorkspaceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Follows the open chat into the workspace it was moved to.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A move is no other chat being opened, so the message being written stays in the composer,
|
||||
/// whether the user moved the chat from here or through the workspace tree. What has to change are
|
||||
/// the paths of its attachments: the move took the chat directory along, and with it the
|
||||
/// transcripts of audio and video files. Left with their old paths, the composer would send
|
||||
/// references to files which are gone.
|
||||
/// </remarks>
|
||||
/// <param name="previousWorkspaceId">The workspace the chat came from, or Guid.Empty for a temporary chat.</param>
|
||||
private async Task FollowMovedChatAsync(Guid previousWorkspaceId)
|
||||
{
|
||||
var chatId = this.ChatThread!.ChatId;
|
||||
var workspaceId = this.ChatThread.WorkspaceId;
|
||||
var relocatedAttachments = this.ComposerState.FileAttachments
|
||||
.Select(attachment => WorkspaceBehaviour.RelocateAttachmentAfterMove(attachment, chatId, previousWorkspaceId, workspaceId))
|
||||
.ToList();
|
||||
|
||||
this.ComposerState.FileAttachments.Clear();
|
||||
foreach (var attachment in relocatedAttachments)
|
||||
this.ComposerState.FileAttachments.Add(attachment);
|
||||
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
}
|
||||
|
||||
@@ -1558,7 +1609,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.RestoreComposerFromTextBlock(textBlock);
|
||||
this.ChatThread.Remove(block);
|
||||
this.ChatThread.RemoveForEditing(block);
|
||||
this.ChatThread.Remove(lastBlockContent);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
@@ -1574,7 +1625,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.RestoreComposerFromTextBlock(textBlock);
|
||||
this.ChatThread.Remove(block);
|
||||
this.ChatThread.RemoveForEditing(block);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -940,10 +940,16 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
private async Task MoveChatAsync(string? chatPath)
|
||||
{
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
var storedChat = await this.LoadChatAsync(chatPath, false);
|
||||
if (storedChat is null)
|
||||
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:
|
||||
//
|
||||
var chat = this.CurrentChatThread is { } openChat && openChat.ChatId == storedChat.ChatId ? openChat : storedChat;
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
@@ -964,11 +970,8 @@ public partial class Workspaces : MSGComponentBase
|
||||
return;
|
||||
|
||||
await WorkspaceBehaviour.MoveChatAsync(chat, workspaceId);
|
||||
if (this.CurrentChatThread?.ChatId == chat.ChatId)
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
if (ReferenceEquals(chat, this.CurrentChatThread))
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
}
|
||||
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
@@ -1037,31 +1037,60 @@ public static class WorkspaceBehaviour
|
||||
|
||||
/// <summary>Rewrites absolute attachment paths after moving the complete chat directory.</summary>
|
||||
private static void UpdateAttachmentPathsAfterMove(ChatThread chat, string sourceDirectory, string targetDirectory)
|
||||
{
|
||||
foreach (var content in chat.Blocks.Select(block => block.Content).OfType<ContentText>())
|
||||
{
|
||||
for (var index = 0; index < content.FileAttachments.Count; index++)
|
||||
content.FileAttachments[index] = RelocateAttachment(content.FileAttachments[index], sourceDirectory, targetDirectory);
|
||||
}
|
||||
|
||||
//
|
||||
// The transcripts of the message not sent yet live in the chat directory as well. Left with
|
||||
// their old paths, the draft points at a directory which is gone after the move, and the
|
||||
// transcripts would never reach the message.
|
||||
//
|
||||
for (var index = 0; index < chat.PendingMediaTranscripts.Count; index++)
|
||||
chat.PendingMediaTranscripts[index] = (ManagedTranscriptAttachment)RelocateAttachment(chat.PendingMediaTranscripts[index], sourceDirectory, targetDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives an attachment the path it has after its chat moved into another workspace.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Moving a chat moves its whole directory, and with it every file inside, such as the
|
||||
/// transcripts of audio and video files. Whatever still refers to such a file needs its new path:
|
||||
/// the messages and the draft of the chat, which the move takes care of itself, and the composer
|
||||
/// of the open chat, which is what this is for. An attachment outside the chat directory stays as
|
||||
/// it is.
|
||||
/// </remarks>
|
||||
/// <param name="attachment">The attachment as it was before the move.</param>
|
||||
/// <param name="chatId">The chat which was moved.</param>
|
||||
/// <param name="sourceWorkspaceId">The workspace the chat came from, or Guid.Empty for a temporary chat.</param>
|
||||
/// <param name="targetWorkspaceId">The workspace the chat lives in now, or Guid.Empty for a temporary chat.</param>
|
||||
/// <returns>The attachment with its new path, or the attachment itself when the move did not touch its file.</returns>
|
||||
public static FileAttachment RelocateAttachmentAfterMove(FileAttachment attachment, Guid chatId, Guid sourceWorkspaceId, Guid targetWorkspaceId) =>
|
||||
RelocateAttachment(attachment, GetChatDirectory(sourceWorkspaceId, chatId), GetChatDirectory(targetWorkspaceId, chatId));
|
||||
|
||||
/// <summary>Gives an attachment inside a moved directory its path in the target directory.</summary>
|
||||
private static FileAttachment RelocateAttachment(FileAttachment attachment, string sourceDirectory, string targetDirectory)
|
||||
{
|
||||
var sourcePrefix = sourceDirectory.EndsWith(Path.DirectorySeparatorChar)
|
||||
? sourceDirectory
|
||||
: sourceDirectory + Path.DirectorySeparatorChar;
|
||||
|
||||
var pathComparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
foreach (var content in chat.Blocks.Select(block => block.Content).OfType<ContentText>())
|
||||
if (!Path.GetFullPath(attachment.FilePath).StartsWith(sourcePrefix, pathComparison))
|
||||
return attachment;
|
||||
|
||||
var relativePath = Path.GetRelativePath(sourceDirectory, attachment.FilePath);
|
||||
var movedPath = Path.Combine(targetDirectory, relativePath);
|
||||
|
||||
return attachment switch
|
||||
{
|
||||
for (var index = 0; index < content.FileAttachments.Count; index++)
|
||||
{
|
||||
var attachment = content.FileAttachments[index];
|
||||
if (!Path.GetFullPath(attachment.FilePath).StartsWith(sourcePrefix, pathComparison))
|
||||
continue;
|
||||
|
||||
var relativePath = Path.GetRelativePath(sourceDirectory, attachment.FilePath);
|
||||
var movedPath = Path.Combine(targetDirectory, relativePath);
|
||||
|
||||
content.FileAttachments[index] = attachment switch
|
||||
{
|
||||
ManagedTranscriptAttachment managed => managed with { FilePath = movedPath },
|
||||
FileAttachmentImage image => image with { FilePath = movedPath },
|
||||
_ => attachment with { FilePath = movedPath },
|
||||
};
|
||||
}
|
||||
}
|
||||
ManagedTranscriptAttachment managed => managed with { FilePath = movedPath },
|
||||
FileAttachmentImage image => image with { FilePath = movedPath },
|
||||
_ => attachment with { FilePath = movedPath },
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task FinalizeStagedTranscriptsAsync(ChatThread chat, string chatDirectory)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- Improved the installation on Windows: AI Studio is now signed, so Windows shows who published it. For a while, Windows may still ask you to confirm the installation until it knows the app better.
|
||||
- Improved the question whether AI Studio should transcribe your audio and video files. It now waits until you choose Yes or No, so a click next to it no longer drops your files.
|
||||
- Improved how fast AI Studio starts when the configuration of your organization includes tokenizers. AI Studio now checks such a tokenizer only when it has changed.
|
||||
- Improved the button in the chat for moving the chat to a workspace. You can now use it whenever the workspace list lets you move the chat as well, for example, right after you attached an audio or video file and before you sent your first message.
|
||||
- Changed the banner on the start page to a fresh design that shows what AI Studio can do today.
|
||||
- Fixed a warning about the confidence level that stayed in the dialog for adding a local data source, even after you chose an embedding provider that meets it. The dialog now checks again whenever you change either of them, so a warning also appears when you switch to a provider that falls short.
|
||||
- Fixed two confidence levels sharing the same name in the German user interface. Moderate is called "Mäßig" again, and Medium stays "Mittel".
|
||||
@@ -28,6 +29,8 @@
|
||||
- Fixed the log file of AI Studio showing the wrong time for some entries when AI Studio was busy. Each entry now shows when it actually happened.
|
||||
- Fixed AI Studio pausing for a moment while it opened the local index of a data source, such as a mailbox. Other work, for example applying the configuration of your organization, had to wait until then.
|
||||
- Fixed a security issue in how AI Studio prepares images for the Visual Briefing assistant. AI Studio now protects all parts of its internal connection in one central place, and an automated test checks that no part is left out.
|
||||
- Fixed the transcripts of your audio and video files disappearing when you edited your last message in a chat. You no longer need to attach the files again. Thanks, Bernhard, for reporting this issue.
|
||||
- Fixed the message you had not sent yet losing its text or the transcripts of your audio and video files when you moved the chat to a workspace.
|
||||
- Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far.
|
||||
- Upgraded several libraries to improve security.
|
||||
- Upgraded to Rust v1.99.0
|
||||
Reference in new issue
Block a user