mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 05:33:37 +00:00
Added the ability to roll back to certain chat messages and to copy an entire chat
This commit is contained in:
parent
c95cc5bacc
commit
601fe727ba
@ -3262,6 +3262,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347088452"] = "Result"
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?"
|
||||
|
||||
-- Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347725178"] = "Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed."
|
||||
|
||||
-- Yes, remove the AI response and edit it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it"
|
||||
|
||||
@ -3313,12 +3316,18 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2822776450"] = "Export
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments"
|
||||
|
||||
-- Roll back to this response
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3132525321"] = "Roll back to this response"
|
||||
|
||||
-- Cannot render content of type {0} yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet."
|
||||
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit"
|
||||
|
||||
-- Roll Back Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3304283125"] = "Roll Back Chat"
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3424652889"] = "Unknown"
|
||||
|
||||
@ -3331,6 +3340,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blocked
|
||||
-- Do you really want to regenerate this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?"
|
||||
|
||||
-- Yes, roll back the chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3951371697"] = "Yes, roll back the chat"
|
||||
|
||||
-- Remove Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove Message"
|
||||
|
||||
@ -5281,6 +5293,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Are you sure
|
||||
-- Move chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Move chat"
|
||||
|
||||
-- Copy Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1192756314"] = "Copy Chat"
|
||||
|
||||
-- Loading chats...
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Loading chats..."
|
||||
|
||||
@ -5353,6 +5368,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "There is alr
|
||||
-- Please enter a workspace name.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name."
|
||||
|
||||
-- Copy chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3337233722"] = "Copy chat"
|
||||
|
||||
-- Rename
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename"
|
||||
|
||||
@ -12891,3 +12909,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Are you s
|
||||
|
||||
-- Unnamed chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unnamed chat"
|
||||
|
||||
-- Copy of {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3365678931"] = "Copy of {0}"
|
||||
|
||||
-- Empty chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T4019509364"] = "Empty chat"
|
||||
|
||||
@ -350,6 +350,27 @@ public sealed record ChatThread
|
||||
this.Blocks.Remove(block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes every content block after the selected content in conversation order.
|
||||
/// </summary>
|
||||
/// <param name="content">The content to keep as the last block.</param>
|
||||
/// <returns>True when one or more later blocks were removed.</returns>
|
||||
public bool RemoveBlocksAfter(IContent content)
|
||||
{
|
||||
var sortedBlocks = this.Blocks.OrderBy(x => x.Time).ToList();
|
||||
var blockIndex = sortedBlocks.FindIndex(block => ReferenceEquals(block.Content, content));
|
||||
if (blockIndex < 0 || blockIndex == sortedBlocks.Count - 1)
|
||||
return false;
|
||||
|
||||
foreach (var block in sortedBlocks.Skip(blockIndex + 1))
|
||||
{
|
||||
DeleteManagedAttachments(block);
|
||||
this.Blocks.Remove(block);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void DeleteManagedAttachments(ContentBlock block)
|
||||
{
|
||||
if (block.Content is not ContentText textContent)
|
||||
|
||||
@ -70,6 +70,12 @@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (!this.IsLastContentBlock && this.Role is ChatRole.AI && this.RollbackFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Roll back to this response")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Restore" Color="Color.Default" Disabled="@(!this.RollbackEnabled)" OnClick="@this.RollbackBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.RemoveBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Removes this block")" Placement="Placement.Bottom">
|
||||
|
||||
@ -76,6 +76,9 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
[Parameter]
|
||||
public Func<IContent, Task>? RegenerateFunc { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<IContent, Task>? RollbackFunc { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<IContent, Task>? EditLastBlockFunc { get; set; }
|
||||
@ -86,6 +89,9 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
[Parameter]
|
||||
public Func<bool> RegenerateEnabled { get; set; } = () => false;
|
||||
|
||||
[Parameter]
|
||||
public bool RollbackEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What the export offers, used both as the label of the export button and as the title of
|
||||
/// the save dialog.
|
||||
@ -780,6 +786,21 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
if (regenerate.HasValue && regenerate.Value)
|
||||
await this.RegenerateFunc(this.Content);
|
||||
}
|
||||
|
||||
private async Task RollbackBlock()
|
||||
{
|
||||
if (this.RollbackFunc is null || this.Role is not ChatRole.AI || !this.RollbackEnabled)
|
||||
return;
|
||||
|
||||
var rollback = await this.DialogService.ShowMessageBox(
|
||||
T("Roll Back Chat"),
|
||||
T("Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed."),
|
||||
T("Yes, roll back the chat"),
|
||||
T("No, keep it"));
|
||||
|
||||
if (rollback.HasValue && rollback.Value)
|
||||
await this.RollbackFunc(this.Content);
|
||||
}
|
||||
|
||||
private async Task EditLastBlock()
|
||||
{
|
||||
@ -852,4 +873,4 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
await this.DisposeMathContainerIfNeededAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,8 @@
|
||||
IsSecondToLastBlock="@isSecondLastBlock"
|
||||
RegenerateFunc="@this.RegenerateBlock"
|
||||
RegenerateEnabled="@(() => this.IsProviderSelected && this.ChatThread.IsLLMProviderAllowed(this.Provider))"
|
||||
RollbackFunc="@this.RollbackBlock"
|
||||
RollbackEnabled="@(!this.IsCurrentChatStreaming)"
|
||||
EditLastBlockFunc="@this.EditLastBlock"
|
||||
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
|
||||
}
|
||||
|
||||
@ -1394,6 +1394,24 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
await this.SendMessage(reuseLastUserPrompt: true);
|
||||
}
|
||||
|
||||
private async Task RollbackBlock(IContent aiBlock)
|
||||
{
|
||||
if (this.ChatThread is null || this.IsCurrentChatStreaming)
|
||||
return;
|
||||
|
||||
if (!this.ChatThread.RemoveBlocksAfter(aiBlock))
|
||||
return;
|
||||
|
||||
this.ChatThread.AugmentedData = string.Empty;
|
||||
this.ChatThread.AISelectedDataSources = [];
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||
this.hasUnsavedChanges = true;
|
||||
await this.SaveThread();
|
||||
this.tokenTracker?.Nudge();
|
||||
this.StateHasChanged();
|
||||
await this.inputField.FocusAsync();
|
||||
}
|
||||
|
||||
private Task EditLastUserBlock(IContent block)
|
||||
{
|
||||
|
||||
@ -81,6 +81,10 @@ else
|
||||
<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("Copy chat")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.CopyChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
@ -148,4 +152,4 @@ else
|
||||
}
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
}
|
||||
}
|
||||
|
||||
@ -781,6 +781,37 @@ public partial class Workspaces : MSGComponentBase
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
}
|
||||
|
||||
private async Task CopyChatAsync(string? chatPath)
|
||||
{
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
var mediaOwner = MediaImportOwner.ForChat(chat.ChatId);
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId) || this.MediaTranscriptionService.IsBusy(mediaOwner))
|
||||
return;
|
||||
|
||||
if (await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES))
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, T("Are you sure you want to load another chat? All unsaved changes will be lost.") },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Copy Chat"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceChat = this.CurrentChatThread is { } currentChat && currentChat.ChatId == chat.ChatId ? currentChat : chat;
|
||||
var copy = await WorkspaceBehaviour.CopyChatAsync(sourceChat);
|
||||
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
this.CurrentChatThread = copy;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
}
|
||||
|
||||
private async Task RenameWorkspaceAsync(string? workspacePath)
|
||||
{
|
||||
if (workspacePath is null)
|
||||
|
||||
@ -3264,6 +3264,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347088452"] = "Ergebni
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Möchten Sie diese Nachricht wirklich löschen?"
|
||||
|
||||
-- Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347725178"] = "Möchtest du diesen Chat wirklich auf diese KI-Antwort zurücksetzen? Alle späteren Nachrichten und deren Anhänge werden dauerhaft gelöscht."
|
||||
|
||||
-- Yes, remove the AI response and edit it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Ja, entferne die KI-Antwort und bearbeite sie."
|
||||
|
||||
@ -3315,12 +3318,18 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2822776450"] = "KI-Antw
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Anzahl der Anhänge"
|
||||
|
||||
-- Roll back to this response
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3132525321"] = "Zu dieser Antwort zurücksetzen"
|
||||
|
||||
-- Cannot render content of type {0} yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Der Inhaltstyp {0} kann noch nicht angezeigt werden."
|
||||
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Bearbeiten"
|
||||
|
||||
-- Roll Back Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3304283125"] = "Chat zurücksetzen"
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3424652889"] = "Unbekannt"
|
||||
|
||||
@ -3333,6 +3342,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blockie
|
||||
-- Do you really want to regenerate this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Möchten Sie diese Nachricht wirklich neu generieren?"
|
||||
|
||||
-- Yes, roll back the chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3951371697"] = "Ja, Chat zurücksetzen"
|
||||
|
||||
-- Remove Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Nachricht entfernen"
|
||||
|
||||
@ -5283,6 +5295,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Möchten Sie
|
||||
-- Move chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Chat verschieben"
|
||||
|
||||
-- Copy Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1192756314"] = "Chat kopieren"
|
||||
|
||||
-- Loading chats...
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Chats werden geladen..."
|
||||
|
||||
@ -5355,6 +5370,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "Es gibt bere
|
||||
-- Please enter a workspace name.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Bitte geben Sie einen Namen für diesen Arbeitsbereich ein."
|
||||
|
||||
-- Copy chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3337233722"] = "Chat kopieren"
|
||||
|
||||
-- Rename
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Umbenennen"
|
||||
|
||||
@ -12893,3 +12911,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Möchten
|
||||
|
||||
-- Unnamed chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unbenannter Chat"
|
||||
|
||||
-- Copy of {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3365678931"] = "Kopie von {0}"
|
||||
|
||||
-- Empty chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T4019509364"] = "Leerer Chat"
|
||||
|
||||
@ -3264,6 +3264,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347088452"] = "Result"
|
||||
-- Do you really want to remove this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?"
|
||||
|
||||
-- Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347725178"] = "Do you really want to roll back this chat to this AI response? All later messages and their attachments will be permanently removed."
|
||||
|
||||
-- Yes, remove the AI response and edit it
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1350385882"] = "Yes, remove the AI response and edit it"
|
||||
|
||||
@ -3315,12 +3318,18 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T2822776450"] = "Export
|
||||
-- Number of attachments
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3018847255"] = "Number of attachments"
|
||||
|
||||
-- Roll back to this response
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3132525321"] = "Roll back to this response"
|
||||
|
||||
-- Cannot render content of type {0} yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3175548294"] = "Cannot render content of type {0} yet."
|
||||
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3267849393"] = "Edit"
|
||||
|
||||
-- Roll Back Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3304283125"] = "Roll Back Chat"
|
||||
|
||||
-- Unknown
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3424652889"] = "Unknown"
|
||||
|
||||
@ -3333,6 +3342,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3816336467"] = "Blocked
|
||||
-- Do you really want to regenerate this message?
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?"
|
||||
|
||||
-- Yes, roll back the chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3951371697"] = "Yes, roll back the chat"
|
||||
|
||||
-- Remove Message
|
||||
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove Message"
|
||||
|
||||
@ -5283,6 +5295,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Are you sure
|
||||
-- Move chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1133040906"] = "Move chat"
|
||||
|
||||
-- Copy Chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1192756314"] = "Copy Chat"
|
||||
|
||||
-- Loading chats...
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1364857726"] = "Loading chats..."
|
||||
|
||||
@ -5355,6 +5370,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3249036008"] = "There is alr
|
||||
-- Please enter a workspace name.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3288132732"] = "Please enter a workspace name."
|
||||
|
||||
-- Copy chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3337233722"] = "Copy chat"
|
||||
|
||||
-- Rename
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T3355849203"] = "Rename"
|
||||
|
||||
@ -12893,3 +12911,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3043761007"] = "Are you s
|
||||
|
||||
-- Unnamed chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3310482275"] = "Unnamed chat"
|
||||
|
||||
-- Copy of {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T3365678931"] = "Copy of {0}"
|
||||
|
||||
-- Empty chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T4019509364"] = "Empty chat"
|
||||
|
||||
@ -782,6 +782,78 @@ public static class WorkspaceBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<ChatThread> CopyChatAsync(ChatThread sourceChat)
|
||||
{
|
||||
var serializedChat = JsonSerializer.Serialize(sourceChat, JSON_OPTIONS);
|
||||
var copiedChat = JsonSerializer.Deserialize<ChatThread>(serializedChat, JSON_OPTIONS)
|
||||
?? throw new InvalidOperationException("The chat could not be copied.");
|
||||
copiedChat = copiedChat with
|
||||
{
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = string.Format(TB("Copy of {0}"), string.IsNullOrWhiteSpace(sourceChat.Name) ? TB("Empty chat") : sourceChat.Name),
|
||||
};
|
||||
|
||||
var targetDirectory = GetChatDirectory(copiedChat.WorkspaceId, copiedChat.ChatId);
|
||||
try
|
||||
{
|
||||
CopyManagedTranscriptAttachments(copiedChat, targetDirectory);
|
||||
await StoreChatAsync(copiedChat);
|
||||
return copiedChat;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (Directory.Exists(targetDirectory))
|
||||
Directory.Delete(targetDirectory, true);
|
||||
|
||||
InvalidateWorkspaceTreeCache();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyManagedTranscriptAttachments(ChatThread chat, string targetChatDirectory)
|
||||
{
|
||||
var targetTranscriptDirectory = Path.Combine(targetChatDirectory, "attachments", "transcripts");
|
||||
var pathComparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
|
||||
var copiedPaths = new Dictionary<string, ManagedTranscriptAttachment>(pathComparer);
|
||||
|
||||
foreach (var content in chat.Blocks.Select(block => block.Content).OfType<ContentText>())
|
||||
{
|
||||
for (var index = 0; index < content.FileAttachments.Count; index++)
|
||||
{
|
||||
if (content.FileAttachments[index] is ManagedTranscriptAttachment transcript)
|
||||
content.FileAttachments[index] = CopyManagedTranscriptAttachment(chat, transcript, targetTranscriptDirectory, copiedPaths);
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index < chat.PendingMediaTranscripts.Count; index++)
|
||||
chat.PendingMediaTranscripts[index] = CopyManagedTranscriptAttachment(chat, chat.PendingMediaTranscripts[index], targetTranscriptDirectory, copiedPaths);
|
||||
}
|
||||
|
||||
private static ManagedTranscriptAttachment CopyManagedTranscriptAttachment(
|
||||
ChatThread chat,
|
||||
ManagedTranscriptAttachment source,
|
||||
string targetTranscriptDirectory,
|
||||
Dictionary<string, ManagedTranscriptAttachment> copiedPaths)
|
||||
{
|
||||
var sourcePath = Path.GetFullPath(source.FilePath);
|
||||
if (copiedPaths.TryGetValue(sourcePath, out var existingCopy))
|
||||
return existingCopy;
|
||||
|
||||
Directory.CreateDirectory(targetTranscriptDirectory);
|
||||
var targetPath = NextTranscriptPath(chat, targetTranscriptDirectory, source.OriginalFileName);
|
||||
if (File.Exists(sourcePath))
|
||||
File.Copy(sourcePath, targetPath);
|
||||
|
||||
var copiedAttachment = new ManagedTranscriptAttachment(
|
||||
Path.GetFileName(targetPath),
|
||||
targetPath,
|
||||
File.Exists(targetPath) ? new FileInfo(targetPath).Length : source.FileSizeBytes,
|
||||
source.OriginalFileName,
|
||||
false);
|
||||
copiedPaths[sourcePath] = copiedAttachment;
|
||||
return copiedAttachment;
|
||||
}
|
||||
|
||||
/// <summary>Creates a transcript atomically inside an already persisted chat.</summary>
|
||||
/// <param name="chat">Persisted chat that owns the transcript counter.</param>
|
||||
/// <param name="originalPath">Original media path.</param>
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
# v26.9.1, build 256 (2026-09-xx xx:xx UTC)
|
||||
- Added a way to copy an entire chat and continue the copy independently while the original conversation stays unchanged.
|
||||
- Added a way to roll a chat back to an earlier AI response. The response you choose stays, everything after it is permanently removed, and your current draft and attachments remain ready so you can continue from there.
|
||||
- Added tools that AI models can use on their own, starting with Web Search and Read Web Page. When you ask something a model cannot answer from what it knows, it now searches the web, reads the pages it found, and answers with the sources it used. You decide which tools a model may use, right below the message field, and you can watch it work: AI Studio shows which tool is running and, afterward, every call it made with its result. Whether tools are offered at all depends on the model because it has to support them. Read Web Page works right away; for Web Search you pick a search service in the app settings — Tavily or Staan with a free API key, or a SearXNG instance you run yourself. Set up more than one, and they can take turns when one of them finds nothing, or be asked all at once with their results combined. Many thanks to Peer Schütt (`peerschuett`) and Nils Kruthoff (`nilskruthoff`) for building this feature.
|
||||
- Added answers that appear word by word even while the AI uses its tools. You read along as the model writes, including the short note it puts down before it looks something up, and the answer that follows a tool call arrives the same way instead of all at once at the end.
|
||||
- Added safeguards around everything these tools bring back. Anything fetched from the web is treated as untrusted: AI Studio removes instructions hidden in a page before a model reads it and tells you when it did, exactly as it already does for the documents and web pages you load yourself. A model can never point a tool at your own network. Each tool states how much you have to trust a provider before it may be used with it, so your questions do not travel further than you allow. You can adjust that requirement per tool in the app settings.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user