mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 11:27:21 +00:00
Added an optional notice for AI-generated content (#884)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
f5a257270e
commit
249fc778a2
26 files changed
+406
-46
No files matched your search
@@ -83,6 +83,25 @@ public static class Markdown
|
||||
return $"{markdownText}{Environment.NewLine}{new string(openFence.FencedChar, openFence.OpeningFencedCharCount)}";
|
||||
}
|
||||
|
||||
/// <summary>Puts a block of Markdown below a Markdown text, such as its sources or a notice.</summary>
|
||||
/// <param name="markdownText">The Markdown text.</param>
|
||||
/// <param name="block">The block to put below the text, or an empty string when there is none.</param>
|
||||
/// <returns>The text followed by the block.</returns>
|
||||
public static string AppendBlock(string markdownText, string block)
|
||||
{
|
||||
if (block.Length == 0)
|
||||
return markdownText;
|
||||
|
||||
if (markdownText.Length == 0)
|
||||
return block;
|
||||
|
||||
//
|
||||
// The blank line is not cosmetic: it ends a paragraph, a list, a table, or a block quote, so
|
||||
// that the block stands on its own instead of being pulled into the last block of the text.
|
||||
//
|
||||
return $"{CloseOpenCodeFence(markdownText)}{Environment.NewLine}{Environment.NewLine}{block}";
|
||||
}
|
||||
|
||||
public static string RemoveSharedIndentation(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
||||
@@ -20,6 +20,8 @@ public static class PandocExport
|
||||
/// This says nothing to the user: it reports what happened and lets the caller decide. A batch
|
||||
/// run over hundreds of documents would otherwise bury the user under notifications. Pandoc
|
||||
/// must be available, which PandocAvailabilityService.EnsureAvailabilityAsync takes care of.
|
||||
/// The text is converted as it is: whether it ends with the notice that AI generated it is up
|
||||
/// to the caller, which knows what the text is.
|
||||
/// </remarks>
|
||||
/// <param name="rustService">The Rust service, used to build the Pandoc call.</param>
|
||||
/// <param name="markdownText">The Markdown text to convert.</param>
|
||||
@@ -113,6 +115,9 @@ public static class PandocExport
|
||||
/// </summary>
|
||||
/// <param name="rustService">The Rust service, used for the save dialog and for Pandoc.</param>
|
||||
/// <param name="pandocAvailability">Makes sure Pandoc is there and offers its installation.</param>
|
||||
/// <param name="disclosureService">Ends the text with the notice that AI generated it, when that is
|
||||
/// switched on. Null for content which never gets the notice, such as the answer of the prompt
|
||||
/// optimizer, which is meant for another model.</param>
|
||||
/// <param name="dialogTitle">The title of the save dialog. The caller knows what the user is
|
||||
/// looking at, a chat message or the result of an assistant, so the caller names it.</param>
|
||||
/// <param name="format">The format to write. Must be a format which uses Pandoc.</param>
|
||||
@@ -120,7 +125,7 @@ public static class PandocExport
|
||||
/// <param name="fileName">What the document is about, used to suggest a name in the save dialog.
|
||||
/// Null falls back to a generic name.</param>
|
||||
/// <returns>True, when the document was written.</returns>
|
||||
public static async Task<bool> ToDocument(RustService rustService, PandocAvailabilityService pandocAvailability, string dialogTitle, FileExportFormat format, IContent markdownContent, string? fileName = null)
|
||||
public static async Task<bool> ToDocument(RustService rustService, PandocAvailabilityService pandocAvailability, AIGeneratedContentDisclosureService? disclosureService, string dialogTitle, FileExportFormat format, IContent markdownContent, string? fileName = null)
|
||||
{
|
||||
if (!format.UsesPandoc() || format.ToFileTypeFilter() is not { } fileTypeFilter)
|
||||
throw new ArgumentOutOfRangeException(nameof(format), format, "Pandoc cannot write this format.");
|
||||
@@ -136,6 +141,8 @@ public static class PandocExport
|
||||
return false;
|
||||
}
|
||||
|
||||
markdownText = disclosureService?.AddDisclosureToMarkdown(markdownText) ?? markdownText;
|
||||
|
||||
var response = await rustService.SaveFile(dialogTitle, [fileTypeFilter], format.ToSuggestedFileName(fileName));
|
||||
if (response.UserCancelled)
|
||||
{
|
||||
|
||||
@@ -304,6 +304,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
|
||||
// Config: show administration settings?
|
||||
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
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a notice to AI-generated content before it leaves AI Studio, which says that AI generated it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The notice is off by default. Whether a text has to say that AI generated it depends on what happens
|
||||
/// to it afterward, for example, whether it is published, as transparency rules such as the EU AI Act
|
||||
/// ask. Only the user or their organization knows that, so the user switches it on in the app settings,
|
||||
/// or an organization does so for everybody with a configuration plugin.
|
||||
///
|
||||
/// The notice ends the content as a paragraph of its own, in the language of the app. It leaves the
|
||||
/// answer itself untouched, and a reader finds it where the text ends, below the sources as well.
|
||||
/// It is Markdown, like everything else AI Studio copies or exports, which is why it is bold in a
|
||||
/// document and stays readable as plain text.
|
||||
///
|
||||
/// Only content a reader takes as a whole gets the notice: an answer which is copied or exported, and
|
||||
/// the files of a batch run. Where one sentence more breaks what the user wanted, it stays away: a code
|
||||
/// block, a single file or table out of an answer, the table of a batch run, and the Lua code of the
|
||||
/// assistant builder. The prompt of the prompt optimizer goes on to another model instead of a reader,
|
||||
/// and a visual briefing is exported as the very version AI Studio checked, byte for byte, so neither
|
||||
/// of them gets the notice. Messages of the user, transcripts, IDs, and paths are no AI answers.
|
||||
/// </remarks>
|
||||
public sealed class AIGeneratedContentDisclosureService(SettingsManager settingsManager)
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AIGeneratedContentDisclosureService).Namespace, nameof(AIGeneratedContentDisclosureService));
|
||||
|
||||
/// <summary>
|
||||
/// Adds the notice below the given Markdown text, when it is switched on.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The Markdown text which AI generated.</param>
|
||||
/// <returns>The text followed by the notice, or the text itself when the notice is off or there is no text.</returns>
|
||||
public string AddDisclosureToMarkdown(string markdown)
|
||||
{
|
||||
if (!settingsManager.ConfigurationData.App.AddAIGeneratedContentDisclosure || string.IsNullOrWhiteSpace(markdown))
|
||||
return markdown;
|
||||
|
||||
var disclosure = Markdown.EscapeInlineText(TB("This content was generated using artificial intelligence (AI)."));
|
||||
return Markdown.AppendBlock(markdown.TrimEnd('\r', '\n'), $"**{disclosure}**");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user