mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 20:52:11 +00:00
Added a default label to copied AI-generated text
This commit is contained in:
parent
7595def8f3
commit
e15e9a0607
@ -30,6 +30,9 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
|||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected RustService RustService { get; init; } = null!;
|
protected RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
protected AIGeneratedContentDisclosureService DisclosureService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected NavigationManager NavigationManager { get; init; } = null!;
|
protected NavigationManager NavigationManager { get; init; } = null!;
|
||||||
@ -529,7 +532,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
|||||||
|
|
||||||
protected async Task CopyToClipboard()
|
protected async Task CopyToClipboard()
|
||||||
{
|
{
|
||||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
|
await this.DisclosureService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChatThread CreateSendToChatThread()
|
private ChatThread CreateSendToChatThread()
|
||||||
|
|||||||
@ -3166,6 +3166,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAGENTRETRIEVALCONT
|
|||||||
-- Spellchecking is disabled
|
-- Spellchecking is disabled
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"] = "Spellchecking is disabled"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"] = "Spellchecking is disabled"
|
||||||
|
|
||||||
|
-- AI-generated content is labeled
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1096690933"] = "AI-generated content is labeled"
|
||||||
|
|
||||||
-- Do you want to show preview features in the app?
|
-- Do you want to show preview features in the app?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Do you want to show preview features in the app?"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Do you want to show preview features in the app?"
|
||||||
|
|
||||||
@ -3190,6 +3193,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"]
|
|||||||
-- Root certificate bundle path
|
-- Root certificate bundle path
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Root certificate bundle path"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Root certificate bundle path"
|
||||||
|
|
||||||
|
-- Label AI-generated content?
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1503137557"] = "Label AI-generated content?"
|
||||||
|
|
||||||
-- Select the desired behavior for the navigation bar.
|
-- Select the desired behavior for the navigation bar.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar."
|
||||||
|
|
||||||
@ -3298,6 +3304,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"]
|
|||||||
-- Additional root certificates are disabled
|
-- Additional root certificates are disabled
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3985928190"] = "Additional root certificates are disabled"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3985928190"] = "Additional root certificates are disabled"
|
||||||
|
|
||||||
|
-- When enabled, copied AI-generated text and Microsoft Word exports include a bold notice that the content was generated using AI.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3990854800"] = "When enabled, copied AI-generated text and Microsoft Word exports include a bold notice that the content was generated using AI."
|
||||||
|
|
||||||
-- Preselect one of your profiles?
|
-- Preselect one of your profiles?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] = "Preselect one of your profiles?"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] = "Preselect one of your profiles?"
|
||||||
|
|
||||||
@ -3334,6 +3343,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"]
|
|||||||
-- Start page
|
-- Start page
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Start page"
|
||||||
|
|
||||||
|
-- AI-generated content is not labeled
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T807210448"] = "AI-generated content is not labeled"
|
||||||
|
|
||||||
-- Preview feature visibility
|
-- Preview feature visibility
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Preview feature visibility"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Preview feature visibility"
|
||||||
|
|
||||||
@ -8773,6 +8785,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like p
|
|||||||
-- Document
|
-- Document
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
|
||||||
|
|
||||||
|
-- This content was generated using artificial intelligence (AI).
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::AIGENERATEDCONTENTDISCLOSURESERVICE::T3440957850"] = "This content was generated using artificial intelligence (AI)."
|
||||||
|
|
||||||
-- The Assistant Builder context could not be loaded.
|
-- The Assistant Builder context could not be loaded.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "The Assistant Builder context could not be loaded."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "The Assistant Builder context could not be loaded."
|
||||||
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@
|
|||||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@this.ExportToWord"/>
|
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@this.ExportToWord"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
<MudCopyClipboardButton Content="@this.Content" Type="@this.Type" Size="Size.Medium"/>
|
<MudCopyClipboardButton Content="@this.Content" Type="@this.Type" Size="Size.Medium" IsAIGenerated="@(this.Role is ChatRole.AI)"/>
|
||||||
</CardHeaderActions>
|
</CardHeaderActions>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
|
|||||||
@ -91,6 +91,9 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
[Inject]
|
[Inject]
|
||||||
private RustService RustService { get; init; } = null!;
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private AIGeneratedContentDisclosureService DisclosureService { get; init; } = null!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||||
|
|
||||||
@ -548,7 +551,7 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
private async Task ExportToWord()
|
private async Task ExportToWord()
|
||||||
{
|
{
|
||||||
await PandocExport.ToMicrosoftWord(this.RustService, this.DialogService, T("Export Chat to Microsoft Word"), this.Content);
|
await PandocExport.ToMicrosoftWord(this.RustService, this.DisclosureService, this.DialogService, T("Export Chat to Microsoft Word"), this.Content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RegenerateBlock()
|
private async Task RegenerateBlock()
|
||||||
|
|||||||
@ -33,6 +33,12 @@ public partial class MudCopyClipboardButton : ComponentBase
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string TooltipMessage { get; set; } = TB("Copies the content to the clipboard");
|
public string TooltipMessage { get; set; } = TB("Copies the content to the clipboard");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the copied content was generated by AI and should receive the configured disclosure.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool IsAIGenerated { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The size of the button. The default size is small.
|
/// The size of the button. The default size is small.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -44,6 +50,9 @@ public partial class MudCopyClipboardButton : ComponentBase
|
|||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private RustService RustService { get; init; } = null!;
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private AIGeneratedContentDisclosureService DisclosureService { get; init; } = null!;
|
||||||
|
|
||||||
private async Task HandleCopyClick()
|
private async Task HandleCopyClick()
|
||||||
{
|
{
|
||||||
@ -58,7 +67,10 @@ public partial class MudCopyClipboardButton : ComponentBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CopyToClipboard(string textContent)
|
private async Task CopyToClipboard(string textContent)
|
||||||
{
|
{
|
||||||
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent);
|
if (this.IsAIGenerated)
|
||||||
|
await this.DisclosureService.CopyText2Clipboard(this.Snackbar, textContent);
|
||||||
|
else
|
||||||
|
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -73,7 +85,10 @@ public partial class MudCopyClipboardButton : ComponentBase
|
|||||||
{
|
{
|
||||||
case ContentType.TEXT:
|
case ContentType.TEXT:
|
||||||
var textContent = (ContentText) contentToCopy;
|
var textContent = (ContentText) contentToCopy;
|
||||||
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text);
|
if (this.IsAIGenerated)
|
||||||
|
await this.DisclosureService.CopyText2Clipboard(this.Snackbar, textContent.Text);
|
||||||
|
else
|
||||||
|
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
<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.")"/>
|
<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.")"/>
|
||||||
<ConfigurationSelect OptionDescription="@T("Start page")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.StartPage)" Data="@ConfigurationSelectDataFactory.GetStartPageData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.StartPage = selectedValue)" OptionHelp="@this.GetStartPageHelpText()" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.StartPage, out var meta) && meta.IsLocked"/>
|
<ConfigurationSelect OptionDescription="@T("Start page")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.StartPage)" Data="@ConfigurationSelectDataFactory.GetStartPageData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.StartPage = selectedValue)" OptionHelp="@this.GetStartPageHelpText()" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.StartPage, out var meta) && meta.IsLocked"/>
|
||||||
<ConfigurationOption OptionDescription="@T("Show administration settings?")" LabelOn="@T("Administration settings are visible")" LabelOff="@T("Administration settings are not visible")" State="@(() => this.SettingsManager.ConfigurationData.App.ShowAdminSettings)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ShowAdminSettings = updatedState)" OptionHelp="@T("When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShowAdminSettings, out var meta) && meta.IsLocked"/>
|
<ConfigurationOption OptionDescription="@T("Show administration settings?")" LabelOn="@T("Administration settings are visible")" LabelOff="@T("Administration settings are not visible")" State="@(() => this.SettingsManager.ConfigurationData.App.ShowAdminSettings)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.ShowAdminSettings = updatedState)" OptionHelp="@T("When enabled, additional administration options become visible. These options are intended for IT staff to manage organization-wide configuration, e.g. configuring and exporting providers for an entire organization.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShowAdminSettings, out var meta) && meta.IsLocked"/>
|
||||||
|
<ConfigurationOption OptionDescription="@T("Label AI-generated content?")" LabelOn="@T("AI-generated content is labeled")" LabelOff="@T("AI-generated content is not labeled")" State="@(() => this.SettingsManager.ConfigurationData.App.AddAIGeneratedContentDisclosure)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.AddAIGeneratedContentDisclosure = updatedState)" OptionHelp="@T("When enabled, copied AI-generated text and Microsoft Word exports include a bold notice that the content was generated using AI.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.AddAIGeneratedContentDisclosure, out var meta) && meta.IsLocked"/>
|
||||||
<ConfigurationSelect OptionDescription="@T("Preview feature visibility")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="@T("Do you want to show preview features in the app?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreviewVisibility, out var meta) && meta.IsLocked"/>
|
<ConfigurationSelect OptionDescription="@T("Preview feature visibility")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="@T("Do you want to show preview features in the app?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreviewVisibility, out var meta) && meta.IsLocked"/>
|
||||||
|
|
||||||
@if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
|
@if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
|
||||||
|
|||||||
@ -238,6 +238,11 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- Configure whether administration settings are visible in the UI:
|
-- Configure whether administration settings are visible in the UI:
|
||||||
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
|
-- CONFIG["SETTINGS"]["DataApp.ShowAdminSettings"] = true
|
||||||
|
|
||||||
|
-- Configure whether copied and exported AI-generated content includes a disclosure.
|
||||||
|
-- The disclosure is enabled by default and is localized using the active app language.
|
||||||
|
-- CONFIG["SETTINGS"]["DataApp.AddAIGeneratedContentDisclosure"] = true
|
||||||
|
-- CONFIG["SETTINGS"]["DataApp.AddAIGeneratedContentDisclosure.AllowUserOverride"] = false
|
||||||
|
|
||||||
-- Configure the visibility of preview features:
|
-- Configure the visibility of preview features:
|
||||||
-- Allowed values are: NONE, RELEASE_CANDIDATE, BETA, ALPHA, PROTOTYPE, EXPERIMENTAL
|
-- Allowed values are: NONE, RELEASE_CANDIDATE, BETA, ALPHA, PROTOTYPE, EXPERIMENTAL
|
||||||
-- Please note:
|
-- Please note:
|
||||||
|
|||||||
@ -3168,6 +3168,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAGENTRETRIEVALCONT
|
|||||||
-- Spellchecking is disabled
|
-- Spellchecking is disabled
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"] = "Rechtschreibprüfung ist deaktiviert"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"] = "Rechtschreibprüfung ist deaktiviert"
|
||||||
|
|
||||||
|
-- AI-generated content is labeled
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1096690933"] = "KI-generierte Inhalte sind gekennzeichnet"
|
||||||
|
|
||||||
-- Do you want to show preview features in the app?
|
-- Do you want to show preview features in the app?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Möchten Sie Vorschaufunktionen in der App anzeigen lassen?"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Möchten Sie Vorschaufunktionen in der App anzeigen lassen?"
|
||||||
|
|
||||||
@ -3192,6 +3195,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1454730224"]
|
|||||||
-- Root certificate bundle path
|
-- Root certificate bundle path
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Pfad zum Stammzertifikatsbundle"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1471315821"] = "Pfad zum Stammzertifikatsbundle"
|
||||||
|
|
||||||
|
-- Label AI-generated content?
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1503137557"] = "Soll KI-generierter Inhalt gekennzeichnet werden?"
|
||||||
|
|
||||||
-- Select the desired behavior for the navigation bar.
|
-- Select the desired behavior for the navigation bar.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Wählen Sie das gewünschte Verhalten für die Navigationsleiste aus."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Wählen Sie das gewünschte Verhalten für die Navigationsleiste aus."
|
||||||
|
|
||||||
@ -3300,6 +3306,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"]
|
|||||||
-- Additional root certificates are disabled
|
-- Additional root certificates are disabled
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3985928190"] = "Zusätzliche Stammzertifikate sind deaktiviert"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3985928190"] = "Zusätzliche Stammzertifikate sind deaktiviert"
|
||||||
|
|
||||||
|
-- When enabled, copied AI-generated text and Microsoft Word exports include a bold notice that the content was generated using AI.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3990854800"] = "Wenn aktiviert, enthalten kopierter, von KI generierter Text und Microsoft Word-Exporte einen fett gedruckten Hinweis, dass der Inhalt mit KI erstellt wurde."
|
||||||
|
|
||||||
-- Preselect one of your profiles?
|
-- Preselect one of your profiles?
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] = "Möchten Sie eines ihrer Profile vorauswählen?"
|
||||||
|
|
||||||
@ -3336,6 +3345,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T716338721"]
|
|||||||
-- Start page
|
-- Start page
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Startseite"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T78084670"] = "Startseite"
|
||||||
|
|
||||||
|
-- AI-generated content is not labeled
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T807210448"] = "KI-generierte Inhalte sind nicht gekennzeichnet"
|
||||||
|
|
||||||
-- Preview feature visibility
|
-- Preview feature visibility
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Sichtbarkeit der Vorschaufunktion"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] = "Sichtbarkeit der Vorschaufunktion"
|
||||||
|
|
||||||
@ -8775,6 +8787,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source Code
|
|||||||
-- Document
|
-- Document
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument"
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument"
|
||||||
|
|
||||||
|
-- This content was generated using artificial intelligence (AI).
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::AIGENERATEDCONTENTDISCLOSURESERVICE::T3440957850"] = "Dieser Inhalt wurde mit künstlicher Intelligenz (KI) erstellt."
|
||||||
|
|
||||||
-- The Assistant Builder context could not be loaded.
|
-- The Assistant Builder context could not be loaded.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "Der Kontext des Assistenten-Builders konnte nicht geladen werden."
|
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T108292972"] = "Der Kontext des Assistenten-Builders konnte nicht geladen werden."
|
||||||
|
|
||||||
|
|||||||
@ -159,6 +159,7 @@ internal sealed class Program
|
|||||||
builder.Services.AddSingleton(typeof(RuntimeInfoResponse), runtimeInfo);
|
builder.Services.AddSingleton(typeof(RuntimeInfoResponse), runtimeInfo);
|
||||||
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
|
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
|
||||||
builder.Services.AddSingleton<SettingsManager>();
|
builder.Services.AddSingleton<SettingsManager>();
|
||||||
|
builder.Services.AddSingleton<AIGeneratedContentDisclosureService>();
|
||||||
builder.Services.AddSingleton<ThreadSafeRandom>();
|
builder.Services.AddSingleton<ThreadSafeRandom>();
|
||||||
builder.Services.AddSingleton<AIJobService>();
|
builder.Services.AddSingleton<AIJobService>();
|
||||||
builder.Services.AddSingleton<AssistantSessionService>();
|
builder.Services.AddSingleton<AssistantSessionService>();
|
||||||
|
|||||||
@ -154,6 +154,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ShowAdminSettings { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowAdminSettings, false);
|
public bool ShowAdminSettings { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowAdminSettings, false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should copied and exported AI-generated content include a disclosure?
|
||||||
|
/// </summary>
|
||||||
|
public bool AddAIGeneratedContentDisclosure { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AddAIGeneratedContentDisclosure, true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of assistants that should be hidden from the UI.
|
/// List of assistants that should be hidden from the UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public static class PandocExport
|
|||||||
|
|
||||||
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport));
|
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport));
|
||||||
|
|
||||||
public static async Task<bool> ToMicrosoftWord(RustService rustService, IDialogService dialogService, string dialogTitle, IContent markdownContent)
|
public static async Task<bool> ToMicrosoftWord(RustService rustService, AIGeneratedContentDisclosureService disclosureService, IDialogService dialogService, string dialogTitle, IContent markdownContent)
|
||||||
{
|
{
|
||||||
var response = await rustService.SaveFile(dialogTitle, [FileTypes.MS_WORD]);
|
var response = await rustService.SaveFile(dialogTitle, [FileTypes.MS_WORD]);
|
||||||
if (response.UserCancelled)
|
if (response.UserCancelled)
|
||||||
@ -41,6 +41,8 @@ public static class PandocExport
|
|||||||
_ => "Unknown content type. Cannot export to Word."
|
_ => "Unknown content type. Cannot export to Word."
|
||||||
};
|
};
|
||||||
|
|
||||||
|
markdownText = await disclosureService.AddDisclosureToMarkdown(markdownText);
|
||||||
|
|
||||||
// Write text content to a temporary file:
|
// Write text content to a temporary file:
|
||||||
await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText);
|
await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText);
|
||||||
|
|
||||||
|
|||||||
@ -181,6 +181,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
|
|
||||||
// Config: show administration settings?
|
// Config: show administration settings?
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowAdminSettings, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowAdminSettings, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
|
// Config: add a disclosure to copied and exported AI-generated content?
|
||||||
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AddAIGeneratedContentDisclosure, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
// Config: preview features visibility
|
// Config: preview features visibility
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
|
||||||
|
|||||||
@ -268,6 +268,10 @@ public static partial class PluginFactory
|
|||||||
// Check for admin settings visibility:
|
// Check for admin settings visibility:
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowAdminSettings, AVAILABLE_PLUGINS))
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowAdminSettings, AVAILABLE_PLUGINS))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
|
// Check for the AI-generated content disclosure:
|
||||||
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AddAIGeneratedContentDisclosure, AVAILABLE_PLUGINS))
|
||||||
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check for preview visibility:
|
// Check for preview visibility:
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreviewVisibility, AVAILABLE_PLUGINS))
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreviewVisibility, AVAILABLE_PLUGINS))
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rich clipboard content with a plain-text fallback.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="PlainText">The text used by applications that do not accept HTML.</param>
|
||||||
|
/// <param name="HtmlText">The HTML used by rich-text applications.</param>
|
||||||
|
public readonly record struct RichClipboardContent(string PlainText, string HtmlText);
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
using AIStudio.Settings;
|
||||||
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a disclosure to AI-generated text before it leaves AI Studio.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AIGeneratedContentDisclosureService(SettingsManager settingsManager, RustService rustService)
|
||||||
|
{
|
||||||
|
private const string DISCLOSURE_DE = "Dieser Inhalt wurde mit künstlicher Intelligenz (KI) erstellt.";
|
||||||
|
private const string DISCLOSURE_EN = "This content was generated using artificial intelligence (AI).";
|
||||||
|
|
||||||
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AIGeneratedContentDisclosureService).Namespace, nameof(AIGeneratedContentDisclosureService));
|
||||||
|
|
||||||
|
private SettingsManager SettingsManager { get; } = settingsManager;
|
||||||
|
|
||||||
|
private RustService RustService { get; } = rustService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the disclosure to non-empty text when the feature is enabled.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> AddDisclosureToMarkdown(string text)
|
||||||
|
{
|
||||||
|
if (!this.SettingsManager.ConfigurationData.App.AddAIGeneratedContentDisclosure || string.IsNullOrWhiteSpace(text))
|
||||||
|
return text;
|
||||||
|
|
||||||
|
var contentWithoutTrailingLineBreaks = text.TrimEnd('\r', '\n');
|
||||||
|
var disclosure = AIStudio.Tools.Markdown.EscapeInlineText(await this.GetDisclosure());
|
||||||
|
return $"{contentWithoutTrailingLineBreaks}{Environment.NewLine}{Environment.NewLine}**{disclosure}**";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies AI-generated text with the configured disclosure.
|
||||||
|
/// </summary>
|
||||||
|
public async Task CopyText2Clipboard(ISnackbar snackbar, string text)
|
||||||
|
{
|
||||||
|
if (!this.SettingsManager.ConfigurationData.App.AddAIGeneratedContentDisclosure || string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
await this.RustService.CopyText2Clipboard(snackbar, text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentWithoutTrailingLineBreaks = text.TrimEnd('\r', '\n');
|
||||||
|
var disclosure = await this.GetDisclosure();
|
||||||
|
var plainText = $"{contentWithoutTrailingLineBreaks}{Environment.NewLine}{Environment.NewLine}{disclosure}";
|
||||||
|
var htmlText = $"<div style=\"white-space: pre-wrap;\">{WebUtility.HtmlEncode(contentWithoutTrailingLineBreaks)}</div><div style=\"font-weight: bold; margin-top: 1em;\">{WebUtility.HtmlEncode(disclosure)}</div>";
|
||||||
|
await this.RustService.CopyRichText2Clipboard(snackbar, plainText, htmlText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetDisclosure()
|
||||||
|
{
|
||||||
|
var translatedDisclosure = TB("This content was generated using artificial intelligence (AI).");
|
||||||
|
if (!string.Equals(translatedDisclosure, DISCLOSURE_EN, StringComparison.Ordinal))
|
||||||
|
return translatedDisclosure;
|
||||||
|
|
||||||
|
var language = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||||
|
return language.IETFTag.StartsWith("de-", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? DISCLOSURE_DE
|
||||||
|
: translatedDisclosure;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Tools.Rust;
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
namespace AIStudio.Tools.Services;
|
namespace AIStudio.Tools.Services;
|
||||||
@ -47,4 +49,50 @@ public sealed partial class RustService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to copy HTML and its plain-text fallback to the clipboard.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="snackbar">The snackbar to show the result.</param>
|
||||||
|
/// <param name="plainText">The content used by applications that do not accept HTML.</param>
|
||||||
|
/// <param name="htmlText">The content used by applications that accept HTML.</param>
|
||||||
|
public async Task CopyRichText2Clipboard(ISnackbar snackbar, string plainText, string htmlText)
|
||||||
|
{
|
||||||
|
var message = TB("Successfully copied the text to your clipboard");
|
||||||
|
var iconColor = Color.Error;
|
||||||
|
var severity = Severity.Error;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = JsonSerializer.Serialize(new RichClipboardContent(plainText, htmlText), this.jsonRustSerializerOptions);
|
||||||
|
var encryptedText = await content.Encrypt(this.encryptor!);
|
||||||
|
var response = await this.http.PostAsync("/clipboard/set-rich-text", new StringContent(encryptedText.EncryptedData));
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
this.logger!.LogError($"Failed to copy rich text to the clipboard due to a network error: '{response.StatusCode}'");
|
||||||
|
message = TB("Failed to copy the text to your clipboard.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>(this.jsonRustSerializerOptions);
|
||||||
|
if (!state.Success)
|
||||||
|
{
|
||||||
|
this.logger!.LogError("Failed to copy rich text to the clipboard.");
|
||||||
|
message = TB("Failed to copy the text to your clipboard.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
iconColor = Color.Success;
|
||||||
|
severity = Severity.Success;
|
||||||
|
this.logger!.LogDebug("Successfully copied rich text to the clipboard.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
snackbar.Add(message, severity, config =>
|
||||||
|
{
|
||||||
|
config.Icon = Icons.Material.Filled.ContentCopy;
|
||||||
|
config.IconSize = Size.Large;
|
||||||
|
config.IconColor = iconColor;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1 +1,3 @@
|
|||||||
# v26.7.4, build 249 (2026-07-xx xx:xx UTC)
|
# v26.7.4, build 249 (2026-07-xx xx:xx UTC)
|
||||||
|
|
||||||
|
- Added a default label to copied AI-generated text and Microsoft Word exports, making it clear when content was generated using AI. The label follows the app language and can be disabled in the app settings.
|
||||||
@ -4,7 +4,7 @@ use arboard::Clipboard;
|
|||||||
use axum::Json;
|
use axum::Json;
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::api_token::APIToken;
|
use crate::api_token::APIToken;
|
||||||
use crate::encryption::{EncryptedText, ENCRYPTION};
|
use crate::encryption::{EncryptedText, ENCRYPTION};
|
||||||
|
|
||||||
@ -16,6 +16,8 @@ trait ClipboardBackend {
|
|||||||
type Error: Display;
|
type Error: Display;
|
||||||
|
|
||||||
fn set_text(&mut self, text: String) -> Result<(), Self::Error>;
|
fn set_text(&mut self, text: String) -> Result<(), Self::Error>;
|
||||||
|
|
||||||
|
fn set_html(&mut self, html: String, alt_text: String) -> Result<(), Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClipboardBackend for Clipboard {
|
impl ClipboardBackend for Clipboard {
|
||||||
@ -24,6 +26,10 @@ impl ClipboardBackend for Clipboard {
|
|||||||
fn set_text(&mut self, text: String) -> Result<(), Self::Error> {
|
fn set_text(&mut self, text: String) -> Result<(), Self::Error> {
|
||||||
Clipboard::set_text(self, text)
|
Clipboard::set_text(self, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_html(&mut self, html: String, alt_text: String) -> Result<(), Self::Error> {
|
||||||
|
Clipboard::set_html(self, html, Some(alt_text))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
@ -71,6 +77,37 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_html_with_retry<B, F>(
|
||||||
|
clipboard: &mut Option<B>,
|
||||||
|
html: String,
|
||||||
|
alt_text: String,
|
||||||
|
mut create_clipboard: F,
|
||||||
|
) -> Result<(), ClipboardOperationError<B::Error>>
|
||||||
|
where
|
||||||
|
B: ClipboardBackend,
|
||||||
|
F: FnMut() -> Result<B, B::Error>,
|
||||||
|
{
|
||||||
|
if clipboard.is_none() {
|
||||||
|
*clipboard = Some(create_clipboard().map_err(ClipboardOperationError::Initialization)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first_result = clipboard.as_mut().unwrap().set_html(html.clone(), alt_text.clone());
|
||||||
|
if let Err(first_error) = first_result {
|
||||||
|
warn!(Source = "Clipboard"; "Failed to set rich text using the current clipboard backend; reinitializing it once: {first_error}.");
|
||||||
|
*clipboard = None;
|
||||||
|
|
||||||
|
let mut retry_clipboard = create_clipboard().map_err(ClipboardOperationError::Initialization)?;
|
||||||
|
if let Err(retry_error) = retry_clipboard.set_html(html, alt_text) {
|
||||||
|
error!(Source = "Clipboard"; "Failed to set rich text after reinitializing the clipboard backend: {retry_error}.");
|
||||||
|
return Err(ClipboardOperationError::Write(retry_error));
|
||||||
|
}
|
||||||
|
|
||||||
|
*clipboard = Some(retry_clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn release_clipboard<B>(clipboard: &mut Option<B>) -> bool {
|
fn release_clipboard<B>(clipboard: &mut Option<B>) -> bool {
|
||||||
clipboard.take().is_some()
|
clipboard.take().is_some()
|
||||||
}
|
}
|
||||||
@ -111,6 +148,51 @@ pub async fn set_clipboard(_token: APIToken, encrypted_text: String) -> Json<Set
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets rich clipboard content with a plain-text fallback.
|
||||||
|
pub async fn set_rich_clipboard(_token: APIToken, encrypted_text: String) -> Json<SetClipboardResponse> {
|
||||||
|
let encrypted_text = EncryptedText::new(encrypted_text);
|
||||||
|
let decrypted_text = match ENCRYPTION.decrypt(&encrypted_text) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => {
|
||||||
|
error!(Source = "Clipboard"; "Failed to decrypt rich clipboard content: {e}.");
|
||||||
|
return Json(SetClipboardResponse {
|
||||||
|
success: false,
|
||||||
|
issue: e,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let content: RichClipboardContent = match serde_json::from_str(&decrypted_text) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(e) => {
|
||||||
|
error!(Source = "Clipboard"; "Failed to deserialize rich clipboard content: {e}.");
|
||||||
|
return Json(SetClipboardResponse {
|
||||||
|
success: false,
|
||||||
|
issue: e.to_string(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut clipboard = CLIPBOARD.lock().unwrap();
|
||||||
|
match set_html_with_retry(&mut clipboard, content.html_text, content.plain_text, Clipboard::new) {
|
||||||
|
Ok(_) => {
|
||||||
|
debug!(Source = "Clipboard"; "Rich text was set to the clipboard successfully.");
|
||||||
|
Json(SetClipboardResponse {
|
||||||
|
success: true,
|
||||||
|
issue: String::from(""),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
error!(Source = "Clipboard"; "Rich clipboard operation failed: {e}.");
|
||||||
|
Json(SetClipboardResponse {
|
||||||
|
success: false,
|
||||||
|
issue: e.to_string(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Releases the process-wide clipboard instance during application shutdown.
|
/// Releases the process-wide clipboard instance during application shutdown.
|
||||||
pub fn shutdown_clipboard() {
|
pub fn shutdown_clipboard() {
|
||||||
let mut clipboard = CLIPBOARD.lock().unwrap();
|
let mut clipboard = CLIPBOARD.lock().unwrap();
|
||||||
@ -126,12 +208,18 @@ pub struct SetClipboardResponse {
|
|||||||
issue: String,
|
issue: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RichClipboardContent {
|
||||||
|
plain_text: String,
|
||||||
|
html_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use super::{ClipboardOperationError, release_clipboard, set_text_with_retry, ClipboardBackend};
|
use super::{ClipboardOperationError, release_clipboard, set_html_with_retry, set_text_with_retry, ClipboardBackend};
|
||||||
|
|
||||||
struct MockClipboard {
|
struct MockClipboard {
|
||||||
id: usize,
|
id: usize,
|
||||||
@ -151,6 +239,15 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_html(&mut self, html: String, alt_text: String) -> Result<(), Self::Error> {
|
||||||
|
self.writes.lock().unwrap().push((self.id, format!("{html}|{alt_text}")));
|
||||||
|
if self.fail_write {
|
||||||
|
Err(format!("backend {} failed", self.id))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for MockClipboard {
|
impl Drop for MockClipboard {
|
||||||
@ -249,6 +346,41 @@ mod tests {
|
|||||||
assert_eq!(*factory.writes.lock().unwrap(), vec![(0, "text".to_string()), (1, "text".to_string())]);
|
assert_eq!(*factory.writes.lock().unwrap(), vec![(0, "text".to_string()), (1, "text".to_string())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn writes_rich_text_with_plain_text_fallback() {
|
||||||
|
let mut clipboard = None;
|
||||||
|
let mut factory = MockFactory::new([false]);
|
||||||
|
|
||||||
|
set_html_with_retry(&mut clipboard, "<strong>notice</strong>".to_string(), "notice".to_string(), || factory.create()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(factory.created, 1);
|
||||||
|
assert_eq!(*factory.writes.lock().unwrap(), vec![(0, "<strong>notice</strong>|notice".to_string())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retries_rich_text_once_with_a_new_instance_after_a_write_failure() {
|
||||||
|
let mut clipboard = None;
|
||||||
|
let mut factory = MockFactory::new([true, false]);
|
||||||
|
|
||||||
|
set_html_with_retry(&mut clipboard, "<strong>notice</strong>".to_string(), "notice".to_string(), || factory.create()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(factory.created, 2);
|
||||||
|
assert_eq!(clipboard.as_ref().unwrap().id, 1);
|
||||||
|
assert_eq!(*factory.writes.lock().unwrap(), vec![(0, "<strong>notice</strong>|notice".to_string()), (1, "<strong>notice</strong>|notice".to_string())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_the_rich_text_retry_error_and_discards_the_failed_instance() {
|
||||||
|
let mut clipboard = None;
|
||||||
|
let mut factory = MockFactory::new([true, true]);
|
||||||
|
|
||||||
|
let error = set_html_with_retry(&mut clipboard, "<strong>notice</strong>".to_string(), "notice".to_string(), || factory.create()).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error, ClipboardOperationError::Write("backend 1 failed".to_string()));
|
||||||
|
assert_eq!(factory.created, 2);
|
||||||
|
assert!(clipboard.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reports_reinitialization_failures_and_discards_the_failed_instance() {
|
fn reports_reinitialization_failures_and_discards_the_failed_instance() {
|
||||||
let mut clipboard = None;
|
let mut clipboard = None;
|
||||||
|
|||||||
@ -38,6 +38,7 @@ pub fn start_runtime_api() {
|
|||||||
.route("/system/qdrant-edge/delete-file", post(crate::qdrant_edge_database::delete_qdrant_edge_embedding_by_file))
|
.route("/system/qdrant-edge/delete-file", post(crate::qdrant_edge_database::delete_qdrant_edge_embedding_by_file))
|
||||||
.route("/system/qdrant-edge/delete-store", post(crate::qdrant_edge_database::delete_qdrant_edge_store))
|
.route("/system/qdrant-edge/delete-store", post(crate::qdrant_edge_database::delete_qdrant_edge_store))
|
||||||
.route("/clipboard/set", post(crate::clipboard::set_clipboard))
|
.route("/clipboard/set", post(crate::clipboard::set_clipboard))
|
||||||
|
.route("/clipboard/set-rich-text", post(crate::clipboard::set_rich_clipboard))
|
||||||
.route("/events", get(crate::app_window::get_event_stream))
|
.route("/events", get(crate::app_window::get_event_stream))
|
||||||
.route("/updates/check", get(crate::app_window::check_for_update))
|
.route("/updates/check", get(crate::app_window::check_for_update))
|
||||||
.route("/updates/install", get(crate::app_window::install_update))
|
.route("/updates/install", get(crate::app_window::install_update))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user