From e5c0931a602685df9c05d04b464b8bb8e2a66097 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 10 Nov 2026 13:54:10 +0100 Subject: [PATCH] Added results and exports to the BeyondFacts Lab (#1184) --- .../BeyondFactsLabAssistant.razor | 13 +- .../BeyondFactsLabAssistant.razor.Steps.cs | 4 +- .../BeyondFactsLabJsonContext.cs | 1 + .../BeyondFactsLabStore.Results.cs | 129 ++++++ .../BeyondFactsLab/BflConversationMessage.cs | 8 + .../BeyondFactsLab/BflHiddenRequest.cs | 50 +++ .../BeyondFactsLab/BflIntroduction.razor | 3 + .../BeyondFactsLab/BflPromptComposer.cs | 23 + .../BeyondFactsLab/BflPromptLibrary.cs | 34 +- .../BeyondFactsLab/BflPromptTemplate.cs | 5 + .../BeyondFactsLab/BflReflectionFolder.cs | 188 ++++++++ .../BflReflectionFolderInput.cs | 15 + .../Assistants/BeyondFactsLab/BflResult.cs | 70 +++ .../BeyondFactsLab/BflResultGenerator.cs | 251 +++++++++++ .../BeyondFactsLab/BflResultKind.cs | 37 ++ .../BeyondFactsLab/BflResultKindExtensions.cs | 68 +++ .../BeyondFactsLab/BflResultLocation.cs | 14 + .../BeyondFactsLab/BflResultStatus.cs | 37 ++ .../BeyondFactsLab/BflResultText.cs | 34 ++ .../BeyondFactsLab/BflResultVersion.cs | 26 ++ .../BeyondFactsLab/BflResults.razor | 159 +++++++ .../BeyondFactsLab/BflResults.razor.cs | 413 ++++++++++++++++++ .../Assistants/BeyondFactsLab/BflStaleness.cs | 34 +- .../BeyondFactsLab/BflWorkStateGenerator.cs | 40 +- .../BeyondFactsLab/Prompts/onboarding.md | 1 + .../Prompts/results/conversation-guide.md | 23 + .../Prompts/results/open-points.md | 21 + .../Prompts/results/position-paper.md | 23 + .../Prompts/results/questions-and-answers.md | 19 + .../Prompts/steps/07-results.md | 8 +- .../Assistants/I18N/allTexts.lua | 250 ++++++++++- app/MindWork AI Studio/Pages/Assistants.razor | 2 +- .../plugin.lua | 250 ++++++++++- .../plugin.lua | 250 ++++++++++- app/MindWork AI Studio/Program.cs | 1 + .../Tools/AssistantCategoryExtensions.cs | 2 +- .../BeyondFactsLabStoreResultTests.cs | 134 ++++++ .../BeyondFactsLab/BflPromptLibraryTests.cs | 2 + .../BflReflectionFolderTests.cs | 200 +++++++++ .../BeyondFactsLab/BflResultRequestTests.cs | 81 ++++ app/Tests/BeyondFactsLab/BflResultTests.cs | 175 ++++++++ app/Tests/Serialization/JsonTestRoots.cs | 3 +- 42 files changed, 3034 insertions(+), 67 deletions(-) create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabStore.Results.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflConversationMessage.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflHiddenRequest.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolder.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolderInput.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResult.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultGenerator.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultKind.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultKindExtensions.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultLocation.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultStatus.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultText.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultVersion.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResults.razor create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResults.razor.cs create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/conversation-guide.md create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/open-points.md create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/position-paper.md create mode 100644 app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/questions-and-answers.md create mode 100644 app/Tests/BeyondFactsLab/BeyondFactsLabStoreResultTests.cs create mode 100644 app/Tests/BeyondFactsLab/BflReflectionFolderTests.cs create mode 100644 app/Tests/BeyondFactsLab/BflResultRequestTests.cs create mode 100644 app/Tests/BeyondFactsLab/BflResultTests.cs diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabAssistant.razor b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabAssistant.razor index d2556dce4..665eea59d 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabAssistant.razor +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabAssistant.razor @@ -152,9 +152,20 @@ WorkStates="@this.workStates" Forms="@this.forms" FormChanged="@(() => this.SaveFormAsync(this.activeStep))" - WorkStateStored="@this.WorkStateStoredAsync" + WorkStateStored="@this.ProjectStoredAsync" Provider="@this.providerSettings" ProviderIssue="@this.ValidateProvider(this.providerSettings)"/> + + @if (this.activeStep is BflStep.RESULTS) + { + + } diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabAssistant.razor.Steps.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabAssistant.razor.Steps.cs index ed94cd40f..6aeb0fef9 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabAssistant.razor.Steps.cs +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabAssistant.razor.Steps.cs @@ -46,10 +46,10 @@ public partial class BeyondFactsLabAssistant } /// - /// Shows a work state after the panel of its step stored it. + /// Shows the new time of change of a preparation after a work state or a result of it was stored. /// /// The preparation with its new time of change. - private Task WorkStateStoredAsync(BflProject project) + private Task ProjectStoredAsync(BflProject project) { this.ReplaceProject(project); return Task.CompletedTask; diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabJsonContext.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabJsonContext.cs index 3d096b19b..633a44d54 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabJsonContext.cs +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabJsonContext.cs @@ -10,6 +10,7 @@ namespace AIStudio.Assistants.BeyondFactsLab; [JsonSerializable(typeof(BflStepFormData))] [JsonSerializable(typeof(BflMaterial))] [JsonSerializable(typeof(BflWorkState))] +[JsonSerializable(typeof(BflResult))] // The preparation selected last: [JsonSerializable(typeof(Guid?))] diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabStore.Results.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabStore.Results.cs new file mode 100644 index 000000000..4e0a764e3 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BeyondFactsLabStore.Results.cs @@ -0,0 +1,129 @@ +using System.Text.Json; + +namespace AIStudio.Assistants.BeyondFactsLab; + +public sealed partial class BeyondFactsLabStore +{ + /// + /// The folder of a preparation which holds its results, one file for each kind. + /// + private const string RESULTS_DIRECTORY_NAME = "results"; + + /// + /// Loads a result of a preparation. + /// + /// The ID of the preparation. + /// The kind of result. + /// The cancellation token. + /// The result, or an empty one while the preparation has none. + /// This version cannot open the result. + public async Task LoadResultAsync(Guid projectId, BflResultKind kind, CancellationToken token = default) + { + await this.InitializeAsync(token); + return await this.LoadResultWithoutInitializeAsync(projectId, kind, token); + } + + /// + /// Loads every result of a preparation. + /// + /// The ID of the preparation. + /// The cancellation token. + /// The results this version can open, by their kinds. + public async Task> LoadResultsAsync(Guid projectId, CancellationToken token = default) + { + await this.InitializeAsync(token); + var results = new Dictionary(); + foreach (var kind in BflResultKind.All) + { + try + { + results[kind] = await this.LoadResultWithoutInitializeAsync(projectId, kind, token); + } + catch (InvalidDataException exception) + { + logger.LogWarning(exception, "Could not open the result '{Kind}' of the preparation '{ProjectId}' of the BeyondFacts Lab.", kind, projectId); + } + } + + return results; + } + + /// + /// Stores a result of a preparation and counts it as a change of the preparation. + /// + /// The ID of the preparation. + /// The kind of result. + /// The result. + /// The cancellation token. + /// The preparation with its new time of change. + /// The preparation does not exist. + /// This version cannot open the preparation or the stored result, so it does not write them either. + public async Task SaveResultAsync(Guid projectId, BflResultKind kind, BflResult result, CancellationToken token = default) + { + await this.InitializeAsync(token); + var gate = this.GetLock(projectId); + await gate.WaitAsync(token); + try + { + var project = await this.LoadRequiredWithoutInitializeAsync(projectId, token); + + // A result which a newer version stored, or which is damaged, stays as it is: + await this.LoadResultWithoutInitializeAsync(projectId, kind, token); + + result.SchemaVersion = BflResult.CURRENT_SCHEMA_VERSION; + await WriteTextAtomicAsync(this.ResultPath(projectId, kind), JsonSerializer.Serialize(result, JSON_OPTIONS), token); + + project.ModifiedAtUtc = DateTimeOffset.UtcNow; + await this.StoreProjectAsync(project, token); + return project; + } + finally + { + gate.Release(); + } + } + + /// + /// Reads a result of a preparation without the first start of the store. + /// + /// The ID of the preparation. + /// The kind of result. + /// The cancellation token. + /// The result, or an empty one when there is no file. + /// This version cannot open the result. + private async Task LoadResultWithoutInitializeAsync(Guid projectId, BflResultKind kind, CancellationToken token) + { + var path = this.ResultPath(projectId, kind); + if (!File.Exists(path)) + return new BflResult(); + + BflResult? result; + try + { + await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, true); + result = await JsonSerializer.DeserializeAsync(stream, JSON_OPTIONS, token); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException) + { + throw new InvalidDataException("This version of AI Studio cannot open the result.", exception); + } + + if (result is null || result.SchemaVersion is < 1 or > BflResult.CURRENT_SCHEMA_VERSION || !IsReadable(result.Draft) || !IsReadable(result.Confirmed)) + throw new InvalidDataException("This version of AI Studio cannot open the result."); + + return result; + } + + /// + /// Tells whether a version of a result, which may be missing, has everything this version of AI Studio reads. + /// + /// The version, or null. + private static bool IsReadable(BflResultVersion? version) => version is null || !IsNull(version.Text) && !IsNull(version.WorkStateRevisions); + + /// + /// Gets the file of a result of a preparation. + /// + /// The ID of the preparation. + /// The kind of result. + private string ResultPath(Guid projectId, BflResultKind kind) => Path.Combine(this.ProjectDirectory(projectId), RESULTS_DIRECTORY_NAME, kind.FileName); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflConversationMessage.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflConversationMessage.cs new file mode 100644 index 000000000..352be5718 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflConversationMessage.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// A message of a conversation in the lab as the person saw it, e.g., for the appendix of the reflection folder. +/// +/// Whether the person wrote the message; otherwise, the lab did. +/// The text of the message, in Markdown. +public sealed record BflConversationMessage(bool IsFromPerson, string Text); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflHiddenRequest.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflHiddenRequest.cs new file mode 100644 index 000000000..27267250a --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflHiddenRequest.cs @@ -0,0 +1,50 @@ +using AIStudio.Chat; +using AIStudio.Provider; + +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// Sends a request which the person never sees, in a hidden copy of a conversation of the lab, e.g., for a work state +/// or a result. +/// +/// +/// The copy carries the system prompt and the messages of the conversation, so the model knows all the step knows. +/// The lab never stores it, and the conversation itself stays as it is. +/// +public static class BflHiddenRequest +{ + /// + /// Sends a request in the hidden copy of a conversation and waits for the whole answer. + /// + /// The hidden copy of the conversation, which keeps the request and the answer. + /// The provider which answers. + /// The request. + /// The images of the material, which go along with the first message. + /// The cancellation token. + /// The answer, empty when the provider failed. + public static async Task AskAsync(ChatThread hidden, AIStudio.Settings.Provider provider, string text, IReadOnlyList images, CancellationToken token) + { + var request = new ContentText { Text = text }; + hidden.Blocks.Add(new ContentBlock + { + Time = DateTimeOffset.Now, + ContentType = ContentType.TEXT, + Role = ChatRole.USER, + Content = request, + HideFromUser = true + }); + + var answer = new ContentText { InitialRemoteWait = true }; + hidden.Blocks.Add(new ContentBlock + { + Time = DateTimeOffset.Now, + ContentType = ContentType.TEXT, + Role = ChatRole.AI, + Content = answer, + HideFromUser = true + }); + + await answer.CreateFromProviderAsync(provider.CreateProvider(), provider.Model, request, BflConversationService.WithImages(hidden, images), token); + return answer.Text; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflIntroduction.razor b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflIntroduction.razor index 2f3dace73..2a3f81686 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflIntroduction.razor +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflIntroduction.razor @@ -40,6 +40,9 @@ @TA("Once a step is largely clear, let the lab create its work state below the conversation. You check and edit it, and once you confirm it, the later steps build on it. The lab also suggests values for empty fields of 'Your details', which you take over one by one.") + + @TA("In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document.") + @T("How to recognize good results") diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptComposer.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptComposer.cs index 46efda18c..7be43f53b 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptComposer.cs +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptComposer.cs @@ -71,6 +71,29 @@ public static class BflPromptComposer [BflPromptTemplate.FIELDS] = SuggestableFields(step, form) }).ReplaceLineEndings(LINE_BREAK.ToString()).Trim(); + /// + /// Puts together the request which lets the model write a result, at the end of a hidden copy of the conversation + /// of the last step. + /// + /// The request, see BflPromptLibrary.ResultPath. + /// The work states of the preparation, by their steps, to name the steps without a confirmed one. + /// The request. + /// The request has a placeholder without a value. + public static string ComposeResultRequest(string template, IReadOnlyDictionary workStates) => BflPromptTemplate.Fill(template, new Dictionary(StringComparer.Ordinal) + { + [BflPromptTemplate.MISSING_WORK_STATES] = MissingWorkStates(workStates) + }).ReplaceLineEndings(LINE_BREAK.ToString()).Trim(); + + /// + /// Names the steps without a confirmed work state, in their order. + /// + /// The work states of the preparation, by their steps. + private static string MissingWorkStates(IReadOnlyDictionary workStates) + { + var missing = BflStep.All.Where(step => workStates.GetValueOrDefault(step)?.Confirmed is null).Select(step => $"„{step.Title}“").ToList(); + return missing.Count is 0 ? "keine; alle Schritte sind bestätigt." : $"{string.Join(", ", missing)}."; + } + /// /// Puts together the request which asks the model once to correct the sections of a work state. /// diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptLibrary.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptLibrary.cs index c5396ed6c..4a25d048f 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptLibrary.cs +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptLibrary.cs @@ -40,6 +40,11 @@ public static class BflPromptLibrary /// public const string WORK_STATE_PATH = FOLDER + "/Prompts/work-state.md"; + /// + /// The folder of the requests which let the model write a result, one for each kind a model writes. + /// + private const string RESULTS_FOLDER = FOLDER + "/Prompts/results/"; + /// /// The instruction to address the person formally. /// @@ -77,6 +82,12 @@ public static class BflPromptLibrary BflPromptTemplate.SECTIONS, BflPromptTemplate.FIELDS); + /// + /// The placeholders BflPromptComposer fills in the request for a result. + /// + private static readonly FrozenSet RESULT_PLACEHOLDERS = FrozenSet.Create(StringComparer.Ordinal, + BflPromptTemplate.MISSING_WORK_STATES); + /// /// The placeholders of every other file: none, because BflPromptComposer inserts these files as values. /// @@ -88,6 +99,21 @@ public static class BflPromptLibrary /// The step. public static string StepPath(BflStep step) => $"{FOLDER}/Prompts/steps/{step.FolderName}.md"; + /// + /// Gets the request which lets the model write a result. + /// + /// The kind of result, one a model writes. + /// No model writes this kind of result. + public static string ResultPath(BflResultKind kind) => kind switch + { + BflResultKind.POSITION_PAPER => RESULTS_FOLDER + "position-paper.md", + BflResultKind.CONVERSATION_GUIDE => RESULTS_FOLDER + "conversation-guide.md", + BflResultKind.QUESTIONS_AND_ANSWERS => RESULTS_FOLDER + "questions-and-answers.md", + BflResultKind.OPEN_POINTS => RESULTS_FOLDER + "open-points.md", + + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "No model writes this result.") + }; + /// /// Gets the instruction for a form of address. /// @@ -103,6 +129,7 @@ public static class BflPromptLibrary KNOWLEDGE_PATH, ONBOARDING_PATH, WORK_STATE_PATH, + ..BflResultKind.All.Where(kind => kind.IsWrittenByModel).Select(ResultPath), ..Enum.GetValues().Select(AddressPath).Distinct(), ..BflStep.All.Select(StepPath) ]; @@ -118,9 +145,9 @@ public static class BflPromptLibrary /// Gets the placeholders a file of the lab's prompts may contain. /// /// - /// BflPromptComposer fills placeholders in the core, in the prompt of the onboarding, and in the request for a work - /// state only. It inserts every other file as a value, whose placeholders it leaves as they are, so that they would - /// reach the model as text. + /// BflPromptComposer fills placeholders in the core, in the prompt of the onboarding, and in the requests for a work + /// state and for a result only. It inserts every other file as a value, whose placeholders it leaves as they are, so + /// that they would reach the model as text. /// /// The file, one of the paths of this class. public static IReadOnlySet PlaceholdersOf(string path) => path switch @@ -128,6 +155,7 @@ public static class BflPromptLibrary CORE_PATH => CORE_PLACEHOLDERS, ONBOARDING_PATH => ONBOARDING_PLACEHOLDERS, WORK_STATE_PATH => WORK_STATE_PLACEHOLDERS, + _ when path.StartsWith(RESULTS_FOLDER, StringComparison.Ordinal) => RESULT_PLACEHOLDERS, _ => NO_PLACEHOLDERS }; diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptTemplate.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptTemplate.cs index f82f561b8..1b7232b6c 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptTemplate.cs +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflPromptTemplate.cs @@ -62,6 +62,11 @@ public static partial class BflPromptTemplate /// public const string FIELDS = "FIELDS"; + /// + /// The steps without a confirmed work state, whose topics a result must not fill with guesses. + /// + public const string MISSING_WORK_STATES = "MISSING_WORK_STATES"; + /// /// Fills every placeholder of a template. /// diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolder.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolder.cs new file mode 100644 index 000000000..9a5af3156 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolder.cs @@ -0,0 +1,188 @@ +using System.Text; + +using AIStudio.Chat; + +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// Puts the reflection folder of a preparation together: all confirmed work states, the open questions, the material, +/// and, if the person likes, the conversations of the steps. +/// +/// +/// No model takes part. The person confirmed every work state word for word, so the folder repeats them word for word +/// instead of having a model rewrite them. A step without a confirmed work state is named, not filled. Personal +/// disclosures stay out unless the person includes them, and so does the conversation of a step which may contain +/// some: the folder is meant to be passed on. +/// +public static class BflReflectionFolder +{ + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BflReflectionFolder).Namespace, nameof(BflReflectionFolder)); + + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(BflReflectionFolder).Namespace, nameof(BflReflectionFolder)); + + /// + /// The key of the section of every step which holds its open points; the folder gathers them in one place. + /// + private const string OPEN_POINTS_KEY = "OPEN_POINTS"; + + /// + /// Puts the reflection folder together. + /// + /// What the folder is put together from. + /// The folder, in Markdown. + public static string Build(BflReflectionFolderInput input) + { + var text = new StringBuilder(); + text.Append("# ").Append(Markdown.EscapeInlineText(BflResultKind.REFLECTION_FOLDER.Title)).Append(": ").AppendLine(Markdown.EscapeInlineText(input.ProjectName)); + + foreach (var step in BflStep.All) + AppendStep(text, step, input); + + AppendOpenPoints(text, input); + AppendMaterials(text, input); + if (input.Conversations.Count > 0) + AppendConversations(text, input); + + return text.ToString().TrimEnd() + "\n"; + } + + /// + /// Gets the messages of a conversation as the person saw them. + /// + /// + /// Hidden messages stay out, such as the request with which the lab opens a conversation, and so does everything + /// but text. + /// + /// The conversation. + /// The visible messages with text, in their order. + public static IReadOnlyList MessagesOf(ChatThread thread) => + [ + .. thread.Blocks + .Where(block => !block.HideFromUser && block.Role is ChatRole.USER or ChatRole.AI) + .Select(block => block.Content is ContentText { Text: var messageText } && !string.IsNullOrWhiteSpace(messageText) ? new BflConversationMessage(block.Role is ChatRole.USER, messageText.Trim()) : null) + .OfType() + ]; + + /// + /// Tells whether a step may hold personal disclosures, in its work state or in its form, so that its conversation + /// may contain them too. + /// + /// The step. + public static bool MayHoldPersonalDisclosures(BflStep step) => BflWorkStateSections.ForStep(step).Any(section => section.IsPersonalDisclosure) || BflFieldCatalog.ForStep(step).Any(field => field.IsPersonalDisclosure); + + /// + /// Appends the confirmed work state of a step, without its open points. + /// + /// The folder so far. + /// The step. + /// What the folder is put together from. + private static void AppendStep(StringBuilder text, BflStep step, BflReflectionFolderInput input) + { + text.AppendLine().Append("## ").AppendLine(Markdown.EscapeInlineText(step.Title)).AppendLine(); + if (input.WorkStates.GetValueOrDefault(step)?.Confirmed is not { } confirmed) + { + text.Append('_').Append(Markdown.EscapeInlineText(TB("No confirmed work state."))).AppendLine("_"); + return; + } + + var confirmedAt = confirmed.ConfirmedAtUtc.ToLocalTime().ToString("d", input.Culture); + text.Append('_').Append(Markdown.EscapeInlineText(string.Format(input.Culture, TB("Version {0}, confirmed on {1}."), confirmed.Revision, confirmedAt))).AppendLine("_"); + var notice = BflStaleness.Of(step, input.WorkStates, input.Forms) switch + { + BflWorkStateStatus.OUTDATED => TB("An earlier step changed after this confirmation."), + BflWorkStateStatus.DETAILS_CHANGED => TB("The details of this step changed after this confirmation."), + _ => string.Empty + }; + + if (notice.Length > 0) + text.AppendLine().Append('_').Append(Markdown.EscapeInlineText(notice)).AppendLine("_"); + + foreach (var section in BflWorkStateSections.ForStep(step)) + { + if (section.Key is OPEN_POINTS_KEY || section.IsPersonalDisclosure && !input.IncludePersonalDisclosures || !confirmed.Sections.TryGetValue(section.Key, out var sectionText) || string.IsNullOrWhiteSpace(sectionText)) + continue; + + text.AppendLine().Append("### ").AppendLine(Markdown.EscapeInlineText(section.Title)).AppendLine().AppendLine(sectionText.Trim()); + } + } + + /// + /// Appends the open points of all confirmed work states, by their steps. + /// + /// The folder so far. + /// What the folder is put together from. + private static void AppendOpenPoints(StringBuilder text, BflReflectionFolderInput input) + { + text.AppendLine().Append("## ").AppendLine(Markdown.EscapeInlineText(TB("Open questions"))).AppendLine(); + var any = false; + foreach (var step in BflStep.All) + { + if (input.WorkStates.GetValueOrDefault(step)?.Confirmed?.Sections.GetValueOrDefault(OPEN_POINTS_KEY) is not { } openPoints || string.IsNullOrWhiteSpace(openPoints)) + continue; + + any = true; + text.Append("### ").AppendLine(Markdown.EscapeInlineText(step.Title)).AppendLine().AppendLine(openPoints.Trim()).AppendLine(); + } + + if (!any) + text.Append('_').Append(Markdown.EscapeInlineText(TB("No confirmed work state names open questions."))).AppendLine("_"); + } + + /// + /// Appends the list of the material. + /// + /// The folder so far. + /// What the folder is put together from. + private static void AppendMaterials(StringBuilder text, BflReflectionFolderInput input) + { + text.AppendLine().Append("## ").AppendLine(Markdown.EscapeInlineText(TB("Material"))).AppendLine(); + if (input.Materials.Count is 0) + { + text.Append('_').Append(Markdown.EscapeInlineText(TB("No material was added."))).AppendLine("_"); + return; + } + + foreach (var material in input.Materials.OrderBy(material => material.AddedAtUtc)) + text.Append("- ").Append(Markdown.EscapeInlineText(material.Name)).Append(" (").Append(Markdown.EscapeInlineText(KindName(material.Kind))).AppendLine(")"); + } + + /// + /// Appends the conversations of the steps as an appendix. + /// + /// The folder so far. + /// What the folder is put together from. + private static void AppendConversations(StringBuilder text, BflReflectionFolderInput input) + { + text.AppendLine().Append("## ").AppendLine(Markdown.EscapeInlineText(TB("Appendix: the conversations"))).AppendLine(); + foreach (var step in BflStep.All) + { + if (!input.Conversations.TryGetValue(step, out var messages) || messages.Count is 0) + continue; + + text.Append("### ").AppendLine(Markdown.EscapeInlineText(step.Title)).AppendLine(); + if (!input.IncludePersonalDisclosures && MayHoldPersonalDisclosures(step)) + { + text.Append('_').Append(Markdown.EscapeInlineText(TB("Left out, as it may contain personal disclosures."))).AppendLine("_").AppendLine(); + continue; + } + + foreach (var message in messages) + { + var speaker = message.IsFromPerson ? TA("You") : TB("Lab"); + text.Append("**").Append(Markdown.EscapeInlineText(speaker)).AppendLine(":**").AppendLine().AppendLine(message.Text).AppendLine(); + } + } + } + + /// + /// Gets the name of a kind of material. + /// + /// The kind of material. + private static string KindName(BflMaterialKind kind) => kind switch + { + BflMaterialKind.DOCUMENT => TB("Document"), + BflMaterialKind.RECORDING => TB("Recording"), + BflMaterialKind.IMAGE => TB("Image"), + _ => TB("Material") + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolderInput.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolderInput.cs new file mode 100644 index 000000000..39d33769b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflReflectionFolderInput.cs @@ -0,0 +1,15 @@ +using System.Globalization; + +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// What the reflection folder of a preparation is put together from. +/// +/// The name of the preparation. +/// The work states of the preparation, by their steps; a step which is missing has none. +/// The forms of the preparation, by their steps, to tell where a work state stands. +/// The material of the preparation which can be opened. +/// The conversations of the steps, by their steps, for the appendix; empty for none. +/// Whether personal disclosures go into the folder, e.g., how the topic touches the person. +/// The culture for dates, the one of the language of the app. +public sealed record BflReflectionFolderInput(string ProjectName, IReadOnlyDictionary WorkStates, IReadOnlyDictionary Forms, IReadOnlyList Materials, IReadOnlyDictionary> Conversations, bool IncludePersonalDisclosures, CultureInfo Culture); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResult.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResult.cs new file mode 100644 index 000000000..12d967e3f --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResult.cs @@ -0,0 +1,70 @@ +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// A result of a preparation, such as the position paper, stored in the folder "results" of the preparation. +/// +/// +/// The lab writes a draft, which the person reads, edits, and confirms; only a confirmed result can be exported. Like +/// a work state, a result keeps its confirmed version while the person works on a new draft. Whether it is outdated +/// is computed from the work states it was built from, see BflStaleness.IsOutdated. +/// +public sealed class BflResult +{ + /// + /// The version of the stored form this version of AI Studio writes. Raise it with every new property. + /// + public const int CURRENT_SCHEMA_VERSION = 1; + + /// + /// Gets or sets the version of the stored form. + /// + public int SchemaVersion { get; set; } = CURRENT_SCHEMA_VERSION; + + /// + /// Gets or sets the draft the person has not confirmed yet, or null while there is none. + /// + public BflResultVersion? Draft { get; set; } + + /// + /// Gets or sets the version the person confirmed, or null while there is none. + /// + public BflResultVersion? Confirmed { get; set; } + + /// + /// Starts a draft from the confirmed version, so that the person can edit it; a draft which is there already + /// stays as it is. + /// + public void Edit() + { + if (this.Draft is not null || this.Confirmed is not { } confirmed) + return; + + this.Draft = new BflResultVersion + { + Text = confirmed.Text, + WorkStateRevisions = new(confirmed.WorkStateRevisions), + ChangedAtUtc = confirmed.ChangedAtUtc, + }; + } + + /// + /// Confirms the draft with the text the person approved: it becomes the version to export, and the draft is gone. + /// + /// The text of the draft as the person approved it. + /// When the person confirmed it. + /// There is no draft to confirm. + public void Confirm(string text, DateTimeOffset confirmedAtUtc) + { + if (this.Draft is not { } draft) + throw new InvalidOperationException("There is no draft of the result to confirm."); + + this.Confirmed = new BflResultVersion + { + Text = text, + WorkStateRevisions = new(draft.WorkStateRevisions), + ChangedAtUtc = confirmedAtUtc, + }; + + this.Draft = null; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultGenerator.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultGenerator.cs new file mode 100644 index 000000000..5c74491a5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultGenerator.cs @@ -0,0 +1,251 @@ +using AIStudio.Provider; +using AIStudio.Tools.AssistantSessions; + +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// Writes the drafts of the results of a preparation: the model writes four of them, and the reflection folder is put +/// together without a model. +/// +/// +/// The model writes in a hidden copy of the conversation of the last step, with its system prompt, so it knows the +/// details, the material, and the confirmed work states of all steps; the person never sees the request, and the chat +/// stays as it is. Like the conversations, a singleton, so that the work goes on while the person looks at another +/// page. Nothing a draft says leaves AI Studio before the person confirms and exports it. +/// +/// The store of the lab. +/// The conversations of the lab, whose prompts and checks the results share. +/// The registry of running assistant sessions. +/// The message bus, for errors. +/// The logger. +public sealed class BflResultGenerator(BeyondFactsLabStore store, BflConversationService conversations, AssistantSessionService sessionService, MessageBus messageBus, ILogger logger) +{ + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(BflResultGenerator).Namespace, nameof(BflResultGenerator)); + + /// + /// The results the model writes right now. + /// + private readonly HashSet running = []; + + /// + /// Guards the results the model writes right now. + /// + private readonly Lock runningLock = new(); + + /// + /// Happens when the model starts or ends writing a result, and when a new draft is stored. + /// + /// + /// Called on the thread of the request. A page renders through InvokeAsync. + /// + public event Func? ResultChanged; + + /// + /// Tells whether the model writes a result right now. + /// + /// The result. + public bool IsRunning(BflResultLocation location) + { + lock (this.runningLock) + return this.running.Contains(location); + } + + /// + /// Lets the model write the draft of a result and stores it, replacing an earlier draft. + /// + /// + /// What goes wrong is reported through the message bus; the method itself does not fail. A confirmed result stays + /// as it is until the person confirms the new draft. + /// + /// The result, one a model writes. + /// The provider which writes the result. + public async Task GenerateAsync(BflResultLocation location, AIStudio.Settings.Provider provider) + { + if (!location.Kind.IsWrittenByModel) + return; + + var stepLocation = new BflStepThreadLocation(location.ProjectId, BflStep.RESULTS); + var conversation = await conversations.GetAsync(stepLocation); + if (conversation.Thread is not { } thread || conversation.IsRunning || !await conversations.CheckProviderAsync(stepLocation, thread, provider)) + return; + + lock (this.runningLock) + { + if (!this.running.Add(location)) + return; + } + + var sessionKey = SessionKey(location); + var sessionId = Guid.Empty; + var status = AssistantSessionStatus.FAILED; + try + { + var workStates = await store.LoadWorkStatesAsync(location.ProjectId); + var revisions = BflStaleness.ConfirmedRevisionsOf(workStates); + var requestMaterial = await conversations.ComposeRequestAsync(stepLocation, provider); + var template = await BflPromptLibrary.ReadAsync(BflPromptLibrary.ResultPath(location.Kind), conversations.PromptOverridesDirectory); + var request = BflPromptComposer.ComposeResultRequest(template, workStates); + + // A copy of the conversation, which the person never sees and the lab never stores: + var hidden = thread with { Blocks = [.. thread.Blocks] }; + BflThreadFactory.PrepareForRequest(hidden, provider, requestMaterial.SystemPrompt); + + var cancellation = new CancellationTokenSource(); + sessionId = (await sessionService.TryBeginAsync(sessionKey, Tools.Components.BEYOND_FACTS_LAB_ASSISTANT.Name, cancellation, null, new(StringComparer.Ordinal))).SessionId; + await this.NotifyAsync(location); + + var text = BflResultText.Read(await BflHiddenRequest.AskAsync(hidden, provider, request, requestMaterial.Images, cancellation.Token)); + if (cancellation.IsCancellationRequested) + { + status = AssistantSessionStatus.CANCELED; + return; + } + + if (text.Length is 0) + { + logger.LogWarning("The model wrote no text for the result '{Location}' of the BeyondFacts Lab.", location.Key); + await messageBus.SendError(new(Icons.Material.Filled.ErrorOutline, TA("The model did not write this material. Please try again, or try another provider."))); + return; + } + + await this.StoreDraftAsync(location, text, revisions); + status = AssistantSessionStatus.COMPLETED; + } + catch (OperationCanceledException) + { + status = AssistantSessionStatus.CANCELED; + } + catch (BflPromptOverrideException exception) + { + logger.LogWarning(exception, "A prompt override of the BeyondFacts Lab has a placeholder which the lab does not fill."); + await messageBus.SendError(new(Icons.Material.Filled.ErrorOutline, string.Format(TA("The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again."), exception.FileName, exception.Placeholder))); + } + catch (ProviderRequestException exception) + { + logger.LogError(exception, "The provider request for a result of the BeyondFacts Lab failed. Status={StatusCode}, Reason='{ReasonPhrase}'.", exception.StatusCode, exception.ReasonPhrase); + await messageBus.SendError(new(Icons.Material.Filled.CloudOff, exception.UserMessage)); + } + catch (FileNotFoundException) + { + // The person deleted the preparation while the model wrote; nothing is left to store. + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException or FormatException) + { + logger.LogError(exception, "The BeyondFacts Lab could not write the result '{Location}'.", location.Key); + await messageBus.SendError(new(Icons.Material.Filled.SaveAs, TA("AI Studio could not create this material. Please try again."))); + } + finally + { + lock (this.runningLock) + this.running.Remove(location); + + if (sessionId != Guid.Empty) + { + await sessionService.CompleteAsync(sessionKey, sessionId, status, string.Empty, null, new(StringComparer.Ordinal)); + await sessionService.ClearAsync(sessionKey); + } + + await this.NotifyAsync(location); + } + } + + /// + /// Puts the reflection folder of a preparation together and stores it as a draft, replacing an earlier draft. + /// + /// + /// What goes wrong is reported through the message bus; the method itself does not fail. + /// + /// The ID of the preparation. + /// The name of the preparation. + /// The forms of the preparation, by their steps, to tell where a work state stands. + /// Whether personal disclosures go into the folder. + /// Whether the conversations of the steps go into the folder as an appendix. + public async Task AssembleReflectionFolderAsync(Guid projectId, string projectName, IReadOnlyDictionary forms, bool includePersonalDisclosures, bool includeConversations) + { + var location = new BflResultLocation(projectId, BflResultKind.REFLECTION_FOLDER); + try + { + var workStates = await store.LoadWorkStatesAsync(projectId); + var materials = (await store.ListMaterialsAsync(projectId)).Select(entry => entry.Material).OfType().ToList(); + var messages = new Dictionary>(); + if (includeConversations) + { + foreach (var step in BflStep.All) + { + if ((await conversations.GetAsync(new BflStepThreadLocation(projectId, step))).Thread is { } thread) + messages[step] = BflReflectionFolder.MessagesOf(thread); + } + } + + var text = BflReflectionFolder.Build(new BflReflectionFolderInput(projectName, workStates, forms, materials, messages, includePersonalDisclosures, Tools.PluginSystem.I18N.I.Culture)); + await this.StoreDraftAsync(location, text, BflStaleness.ConfirmedRevisionsOf(workStates)); + } + catch (FileNotFoundException) + { + // The person deleted the preparation meanwhile; nothing is left to store. + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) + { + logger.LogError(exception, "The BeyondFacts Lab could not put the reflection folder of '{ProjectId}' together.", projectId); + await messageBus.SendError(new(Icons.Material.Filled.SaveAs, TA("AI Studio could not create this material. Please try again."))); + } + finally + { + await this.NotifyAsync(location); + } + } + + /// + /// Stops the model writing a result. + /// + /// The result. + public Task CancelAsync(BflResultLocation location) => sessionService.CancelAsync(SessionKey(location)); + + /// + /// Stores the new draft of a result; a confirmed version stays as it is. + /// + /// The result. + /// The text of the draft. + /// The revisions of the confirmed work states the draft was built from. + private async Task StoreDraftAsync(BflResultLocation location, string text, IReadOnlyDictionary workStateRevisions) + { + var result = await store.LoadResultAsync(location.ProjectId, location.Kind); + result.Draft = new BflResultVersion + { + Text = text, + WorkStateRevisions = new(workStateRevisions), + ChangedAtUtc = DateTimeOffset.UtcNow, + }; + + await store.SaveResultAsync(location.ProjectId, location.Kind, result); + } + + /// + /// Tells every page which listens that a result changed. + /// + /// The result. + private async Task NotifyAsync(BflResultLocation location) + { + if (this.ResultChanged is not { } handlers) + return; + + foreach (var handler in handlers.GetInvocationList().Cast>()) + { + try + { + await handler(location); + } + catch (Exception exception) + { + // A page which went away meanwhile must not stop the work: + logger.LogDebug(exception, "A page of the BeyondFacts Lab could not show a change of a result."); + } + } + } + + /// + /// Gets the key under which writing a result registers in AssistantSessionService, beside the conversations. + /// + /// The result. + private static AssistantSessionKey SessionKey(BflResultLocation location) => new(Tools.Components.BEYOND_FACTS_LAB_ASSISTANT, location.Key); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultKind.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultKind.cs new file mode 100644 index 000000000..ddcd17996 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultKind.cs @@ -0,0 +1,37 @@ +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// The materials a person takes away from a preparation in the BeyondFacts Lab, in the last step. +/// +/// +/// They are called results in the code, since BflMaterial already names what the person uploads. Each of them +/// is stored in a file of its own, see BflResultKindExtensions.FileName. +/// +public enum BflResultKind +{ + /// + /// All confirmed work states, the material, the open questions, and the revisions, put together word for word, + /// without a model. + /// + REFLECTION_FOLDER, + + /// + /// One page on what the person stands for and why. + /// + POSITION_PAPER, + + /// + /// The opening, the key messages, questions to others, and wordings for uncertainty or disagreement. + /// + CONVERSATION_GUIDE, + + /// + /// Critical questions from the test situation with approaches to answering them. + /// + QUESTIONS_AND_ANSWERS, + + /// + /// What the person still has to research, clarify, or decide before the event. + /// + OPEN_POINTS, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultKindExtensions.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultKindExtensions.cs new file mode 100644 index 000000000..83ae27eb9 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultKindExtensions.cs @@ -0,0 +1,68 @@ +namespace AIStudio.Assistants.BeyondFactsLab; + +public static class BflResultKindExtensions +{ + private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BflResultKindExtensions).Namespace, nameof(BflResultKindExtensions)); + + private static string TA(string fallbackEN) => Tools.PluginSystem.I18N.I.TA(fallbackEN, typeof(BflResultKindExtensions).Namespace, nameof(BflResultKindExtensions)); + + /// The kind of result. + extension(BflResultKind kind) + { + /// + /// Gets the name of the result. + /// + public string Title => kind switch + { + BflResultKind.REFLECTION_FOLDER => TB("Reflection folder"), + BflResultKind.POSITION_PAPER => TB("Position paper"), + BflResultKind.CONVERSATION_GUIDE => TB("Conversation guide"), + BflResultKind.QUESTIONS_AND_ANSWERS => TB("Questions and answers"), + BflResultKind.OPEN_POINTS => TB("Open points and next steps"), + + _ => string.Empty, + }; + + /// + /// Gets what the result contains, for the info symbol beside its name. + /// + public string Description => kind switch + { + BflResultKind.REFLECTION_FOLDER => TA("All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix."), + BflResultKind.POSITION_PAPER => TA("One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise."), + BflResultKind.CONVERSATION_GUIDE => TA("The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing."), + BflResultKind.QUESTIONS_AND_ANSWERS => TA("Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer."), + BflResultKind.OPEN_POINTS => TA("What you still have to research, clarify with others, or decide yourself before you appear."), + + _ => string.Empty, + }; + + /// + /// Tells whether a model writes the result. The reflection folder is put together from what the person + /// confirmed, word for word. + /// + public bool IsWrittenByModel => kind is not BflResultKind.REFLECTION_FOLDER; + + /// + /// Gets the name of the file in which a preparation keeps the result. + /// + /// + /// These names are stored on disk, so they never change. + /// + public string FileName => kind switch + { + BflResultKind.REFLECTION_FOLDER => "reflection-folder.json", + BflResultKind.POSITION_PAPER => "position-paper.json", + BflResultKind.CONVERSATION_GUIDE => "conversation-guide.json", + BflResultKind.QUESTIONS_AND_ANSWERS => "questions-and-answers.json", + BflResultKind.OPEN_POINTS => "open-points.json", + + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "The result has no file."), + }; + + /// + /// Gets every kind of result, in the order the last step shows them. + /// + public static IReadOnlyList All => Enum.GetValues(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultLocation.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultLocation.cs new file mode 100644 index 000000000..26aaf5a8b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultLocation.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// Where a result belongs: to a preparation, as one of its kinds of results. +/// +/// The ID of the preparation. +/// The kind of result. +public sealed record BflResultLocation(Guid ProjectId, BflResultKind Kind) +{ + /// + /// Gets a key which tells the result apart from every other one, e.g., for a running request. + /// + public string Key => $"{this.ProjectId:D}/results/{this.Kind.FileName}"; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultStatus.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultStatus.cs new file mode 100644 index 000000000..7fb8b4358 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultStatus.cs @@ -0,0 +1,37 @@ +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// Where a result of a preparation stands, as the header of its panel shows it. +/// +public enum BflResultStatus +{ + /// + /// This version of AI Studio cannot open the result. + /// + UNREADABLE, + + /// + /// The result has neither a draft nor a confirmed version. + /// + NOT_CREATED, + + /// + /// The model writes the result right now. + /// + WRITING, + + /// + /// The result has a draft the person has not confirmed yet. + /// + DRAFT, + + /// + /// The person confirmed the result, and it still fits the work states. + /// + CONFIRMED, + + /// + /// The person confirmed the result, but a work state changed since. + /// + OUTDATED, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultText.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultText.cs new file mode 100644 index 000000000..89df7c488 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultText.cs @@ -0,0 +1,34 @@ +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// Reads the text of a result from the answer of the model. +/// +/// +/// The request asks for plain Markdown, but some models wrap a document in a code block all the same. Such a block +/// would show the result as code and export it as code, so it is taken off; everything else stays as the model wrote +/// it, for the person to read and edit. +/// +public static class BflResultText +{ + /// + /// Reads the text of a result from the answer of the model. + /// + /// The answer of the model. + /// The text, without a code block around all of it; empty when the model wrote nothing. + public static string Read(string answer) + { + var text = answer.ReplaceLineEndings("\n").Trim(); + if (!text.StartsWith("```", StringComparison.Ordinal) && !text.StartsWith("~~~", StringComparison.Ordinal)) + return text; + + var fence = text[..3]; + var firstLineEnd = text.IndexOf('\n'); + if (firstLineEnd < 0 || !text.EndsWith(fence, StringComparison.Ordinal) || text.Length - fence.Length <= firstLineEnd) + return text; + + var inner = text[(firstLineEnd + 1)..^fence.Length]; + + // A fence inside means the block is one of several, not a wrapper around the whole answer: + return inner.Contains(fence, StringComparison.Ordinal) ? text : inner.Trim(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultVersion.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultVersion.cs new file mode 100644 index 000000000..8ca934b3b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResultVersion.cs @@ -0,0 +1,26 @@ +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// One version of a result: a draft, or the version the person confirmed. +/// +public sealed class BflResultVersion +{ + /// + /// Gets or sets the text of the result, in Markdown. + /// + public string Text { get; set; } = string.Empty; + + /// + /// Gets or sets the revisions of the confirmed work states the result was built from, see + /// BflStaleness.ConfirmedRevisionsOf. A step without a confirmed work state at that time is missing. + /// + /// + /// Editing the text keeps them: the person changes the wording, not what the result builds on. + /// + public Dictionary WorkStateRevisions { get; set; } = []; + + /// + /// Gets or sets when the lab wrote the draft, or when the person confirmed the result. + /// + public DateTimeOffset ChangedAtUtc { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResults.razor b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResults.razor new file mode 100644 index 000000000..987875910 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResults.razor @@ -0,0 +1,159 @@ +@inherits MSGComponentBase + +@TA("What would you like to take away?") +@TA("The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document.") + +@if (this.MissingSteps.Count > 0) +{ + + @string.Format(Culture, TA("These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open."), string.Join(", ", this.MissingSteps.Select(step => $"„{step.Title}“"))) + +} + + + @T("Create all materials in parallel") + + +@* Only one material is open at a time, so that the list stays clear: *@ + + @foreach (var kind in BflResultKind.All) + { + var status = this.StatusOf(kind); + + +
+ + @kind.Title + + + + + @this.StatusText(status) +
+
+ + @if (!this.results.TryGetValue(kind, out var result)) + { + + @TA("AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged.") + + } + else + { + @if (this.IsGenerating(kind)) + { + + + @T("The lab writes this material") + @T("Stop") + + } + + @if (result.Draft is { } draft) + { +
+ @T("Draft, not confirmed yet") + @if (BflStaleness.IsOutdated(draft, this.WorkStates)) + { + + @TA("A work state changed since the lab wrote this draft. You can create it anew.") + + } + + @if (this.editing.Contains(kind)) + { + + } + else + { +
+ +
+ } + + + @T("Confirm") + @if (this.editing.Contains(kind)) + { + @T("Show preview") + } + else + { + @T("Edit") + } + @T("Discard draft") + +
+ } + else if (result.Confirmed is { } confirmed) + { + @if (BflStaleness.IsOutdated(confirmed, this.WorkStates)) + { + + @TA("A work state changed since you confirmed this material. Please create it anew, or check whether it still fits.") + + } + else + { + @string.Format(Culture, TA("You confirmed this material on {0}."), confirmed.ChangedAtUtc.ToLocalTime().ToString("g", Culture)) + } + +
+ +
+ + + + @foreach (var format in FileExportFormat.DocumentFormats) + { + + } + + @foreach (var format in FileExportFormat.TextFormats) + { + + } + + @T("Edit") + + } + + @if (kind is BflResultKind.REFLECTION_FOLDER) + { + + @TA("Include personal disclosures") + @TA("Add the conversations as an appendix") + + } + + + @(result.Draft is null && result.Confirmed is null ? T("Create") : T("Create anew")) + + } +
+
+ } +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResults.razor.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResults.razor.cs new file mode 100644 index 000000000..7febb33c9 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflResults.razor.cs @@ -0,0 +1,413 @@ +using System.Globalization; + +using AIStudio.Chat; +using AIStudio.Components; +using AIStudio.Dialogs; +using AIStudio.Tools.Services; + +using Microsoft.AspNetCore.Components; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Assistants.BeyondFactsLab; + +/// +/// The results of a preparation in the last step of the BeyondFacts Lab: what the person takes away, each with its +/// draft, its confirmed version, and its export. +/// +/// +/// The component loads and stores the results itself; the page keeps the work states, which tell whether a result is +/// outdated. The model writes in BflResultGenerator, so that the work goes on while the person looks elsewhere; the +/// component reads the results again whenever the generator says one changed. Only a confirmed result can be exported. +/// +public partial class BflResults : MSGComponentBase +{ + [Inject] + private BeyondFactsLabStore Store { get; init; } = null!; + + [Inject] + private BflResultGenerator Generator { get; init; } = null!; + + [Inject] + private RustService RustService { get; init; } = null!; + + [Inject] + private PandocAvailabilityService PandocAvailability { get; init; } = null!; + + [Inject] + private AIGeneratedContentDisclosureService DisclosureService { get; init; } = null!; + + [Inject] + private IDialogService DialogService { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + /// + /// Gets or sets the ID of the preparation whose results the component shows. + /// + [Parameter, EditorRequired] + public Guid ProjectId { get; set; } + + /// + /// Gets or sets the name of the preparation, for the reflection folder and the names of exported files. + /// + [Parameter, EditorRequired] + public string ProjectName { get; set; } = string.Empty; + + /// + /// Gets or sets the work states of the preparation, by their steps. + /// + [Parameter, EditorRequired] + public IReadOnlyDictionary WorkStates { get; set; } = new Dictionary(); + + /// + /// Gets or sets the forms of the preparation, by their steps, which the reflection folder needs to tell where a + /// work state stands. + /// + [Parameter, EditorRequired] + public IReadOnlyDictionary Forms { get; set; } = new Dictionary(); + + /// + /// Gets or sets the callback raised after a result was stored, with the preparation and its new time of change. + /// + [Parameter] + public EventCallback ResultStored { get; set; } + + /// + /// Gets or sets the provider which writes the results. + /// + [Parameter, EditorRequired] + public AIStudio.Settings.Provider Provider { get; set; } = Settings.Provider.NONE; + + /// + /// Gets or sets why the provider cannot write, or null when it can. + /// + [Parameter] + public string? ProviderIssue { get; set; } + + private static CultureInfo Culture => Tools.PluginSystem.I18N.I.Culture; + + /// + /// The results this version can open, by their kinds. + /// + private Dictionary results = []; + + /// + /// The preparation whose results were loaded, to load again when the page shows another one. + /// + private Guid loadedProjectId; + + /// + /// The result whose panel is open, or null while all are closed. Only one is open at a time. + /// + private BflResultKind? expandedKind = BflResultKind.All[0]; + + /// + /// The results whose draft the person edits as text instead of reading its preview. + /// + private readonly HashSet editing = []; + + /// + /// The results the component stores or puts together right now. + /// + private readonly HashSet saving = []; + + /// + /// Whether the reflection folder takes up personal disclosures. + /// + private bool includePersonalDisclosures; + + /// + /// Whether the reflection folder takes up the conversations of the steps as an appendix. + /// + private bool includeConversations; + + /// + /// Gets the steps without a confirmed work state, in their order. + /// + private IReadOnlyList MissingSteps => [.. BflStep.All.Where(step => this.WorkStates.GetValueOrDefault(step)?.Confirmed is null)]; + + /// + protected override async Task OnInitializedAsync() + { + this.Generator.ResultChanged += this.OnResultChangedAsync; + await base.OnInitializedAsync(); + } + + /// + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + if (this.loadedProjectId == this.ProjectId) + return; + + this.loadedProjectId = this.ProjectId; + this.editing.Clear(); + this.results = []; + await this.LoadAsync(); + } + + /// + protected override void DisposeResources() + { + this.Generator.ResultChanged -= this.OnResultChangedAsync; + base.DisposeResources(); + } + + private bool IsGenerating(BflResultKind kind) => this.Generator.IsRunning(new BflResultLocation(this.ProjectId, kind)); + + private bool IsBusy(BflResultKind kind) => this.saving.Contains(kind) || this.IsGenerating(kind); + + /// + /// Gets the results which "Create all materials in parallel" creates: every one which can be opened and is idle. + /// + private IReadOnlyList CreatableKinds => [.. BflResultKind.All.Where(kind => this.results.ContainsKey(kind) && !this.IsBusy(kind))]; + + private bool CanCreateAll => this.ProviderIssue is null && this.CreatableKinds.Count > 0; + + private bool AnyCreated => this.results.Values.Any(result => result.Draft is not null || result.Confirmed is not null); + + /// + /// Tells where a result stands, for the header of its panel. + /// + /// The kind of result. + private BflResultStatus StatusOf(BflResultKind kind) + { + if (this.IsGenerating(kind)) + return BflResultStatus.WRITING; + + return this.results.GetValueOrDefault(kind) switch + { + null => BflResultStatus.UNREADABLE, + { Draft: not null } => BflResultStatus.DRAFT, + { Confirmed: { } confirmed } => BflStaleness.IsOutdated(confirmed, this.WorkStates) ? BflResultStatus.OUTDATED : BflResultStatus.CONFIRMED, + _ => BflResultStatus.NOT_CREATED + }; + } + + private static string StatusIcon(BflResultStatus status) => status switch + { + BflResultStatus.UNREADABLE => Icons.Material.Filled.Error, + BflResultStatus.WRITING => Icons.Material.Filled.HourglassTop, + BflResultStatus.DRAFT => Icons.Material.Filled.EditNote, + BflResultStatus.CONFIRMED => Icons.Material.Filled.CheckCircle, + BflResultStatus.OUTDATED => Icons.Material.Filled.Update, + _ => Icons.Material.Filled.RadioButtonUnchecked + }; + + private static Color StatusColor(BflResultStatus status) => status switch + { + BflResultStatus.UNREADABLE => Color.Error, + BflResultStatus.WRITING => Color.Primary, + BflResultStatus.DRAFT => Color.Info, + BflResultStatus.CONFIRMED => Color.Success, + BflResultStatus.OUTDATED => Color.Warning, + _ => Color.Default + }; + + private string StatusText(BflResultStatus status) => status switch + { + BflResultStatus.UNREADABLE => T("Cannot be opened"), + BflResultStatus.WRITING => T("Being written"), + BflResultStatus.DRAFT => T("Draft"), + BflResultStatus.CONFIRMED => T("Confirmed"), + BflResultStatus.OUTDATED => T("Outdated"), + _ => T("Not created yet") + }; + + /// + /// Opens the panel of a result and closes the others, or closes it. + /// + /// The kind of result. + /// Whether the panel is open now. + private void SetExpanded(BflResultKind kind, bool expanded) + { + if (expanded) + this.expandedKind = kind; + else if (this.expandedKind == kind) + this.expandedKind = null; + } + + /// + /// Reads the results of the preparation again once the generator started, ended, or stored a draft. + /// + /// The result which changed. + private Task OnResultChangedAsync(BflResultLocation location) + { + if (location.ProjectId != this.ProjectId) + return Task.CompletedTask; + + return this.InvokeAsync(async () => + { + await this.LoadAsync(); + this.StateHasChanged(); + }); + } + + /// + /// Reads the results of the preparation shown. + /// + private async Task LoadAsync() + { + var projectId = this.ProjectId; + var loaded = await this.Store.LoadResultsAsync(projectId); + + // The page may have shown another preparation meanwhile: + if (projectId == this.ProjectId) + this.results = new(loaded); + } + + /// + /// Lets the model write a result, or puts the reflection folder together. + /// + /// The kind of result. + private async Task CreateAsync(BflResultKind kind) + { + this.editing.Remove(kind); + if (kind.IsWrittenByModel) + { + if (this.ProviderIssue is null) + await this.Generator.GenerateAsync(new BflResultLocation(this.ProjectId, kind), this.Provider); + + return; + } + + this.saving.Add(kind); + try + { + await this.Generator.AssembleReflectionFolderAsync(this.ProjectId, this.ProjectName, this.Forms, this.includePersonalDisclosures, this.includeConversations); + } + finally + { + this.saving.Remove(kind); + } + } + + /// + /// Creates every idle result at once: the model writes the four of them in parallel, while AI Studio puts the + /// reflection folder together with the choices of its panel. + /// + /// + /// Like "Create anew", a new draft replaces an earlier one, while a confirmed version stays until the person + /// confirms the new draft. Since a draft may hold edits of the person, the component asks first when there is one. + /// + private async Task CreateAllAsync() + { + var kinds = this.CreatableKinds; + if (this.ProviderIssue is not null || kinds.Count is 0) + return; + + if (kinds.Any(kind => this.results[kind].Draft is not null)) + { + var parameters = new DialogParameters + { + { dialog => dialog.Message, TA("Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts.") } + }; + + var reference = await this.DialogService.ShowAsync(T("Create all materials in parallel"), parameters, DialogOptions.FULLSCREEN); + var answer = await reference.Result; + if (answer is null || answer.Canceled) + return; + } + + await Task.WhenAll(kinds.Select(this.CreateAsync)); + } + + private Task CancelAsync(BflResultKind kind) => this.Generator.CancelAsync(new BflResultLocation(this.ProjectId, kind)); + + private void StartEditing(BflResultKind kind) => this.editing.Add(kind); + + private void ShowPreview(BflResultKind kind) => this.editing.Remove(kind); + + private async Task SetDraftAsync(BflResultKind kind, string? text) + { + if (this.results.GetValueOrDefault(kind) is not { Draft: { } draft } result) + return; + + draft.Text = text ?? string.Empty; + await this.SaveAsync(kind, result); + } + + private async Task ConfirmAsync(BflResultKind kind) + { + if (this.results.GetValueOrDefault(kind) is not { Draft: { } draft } result) + return; + + result.Confirm(draft.Text, DateTimeOffset.UtcNow); + this.editing.Remove(kind); + await this.SaveAsync(kind, result); + } + + private async Task DiscardDraftAsync(BflResultKind kind) + { + if (this.results.GetValueOrDefault(kind) is not { Draft: not null } result) + return; + + result.Draft = null; + this.editing.Remove(kind); + await this.SaveAsync(kind, result); + } + + private async Task EditAsync(BflResultKind kind) + { + if (this.results.GetValueOrDefault(kind) is not { Confirmed: not null } result) + return; + + result.Edit(); + this.editing.Add(kind); + await this.SaveAsync(kind, result); + } + + /// + /// Exports the confirmed version of a result, with the notice that AI generated it when the person or the + /// organization switched that on. + /// + /// + /// The format knows who writes it, as in the chat: Pandoc writes documents and HTML, AI Studio itself writes + /// Markdown. + /// + /// The kind of result. + /// The confirmed version. + /// The format of the file. + private async Task ExportAsync(BflResultKind kind, BflResultVersion confirmed, FileExportFormat format) + { + var fileName = $"{this.ProjectName} - {kind.Title}"; + this.saving.Add(kind); + try + { + if (format.UsesPandoc) + await PandocExport.ToDocument(this.RustService, this.PandocAvailability, this.DisclosureService, kind.Title, format, new ContentText { Text = confirmed.Text }, fileName); + else + await PlainFileExport.ToFile(this.RustService, kind.Title, format, this.DisclosureService.AddDisclosureToMarkdown(confirmed.Text), fileName); + } + catch (ArgumentOutOfRangeException exception) + { + this.Logger.LogError(exception, "Could not export a result of the BeyondFacts Lab, because no exporter writes the format {ExportFormat}.", format); + await this.MessageBus.SendError(new(Icons.Material.Filled.Error, TA("AI Studio could not export this material. Please try another format."))); + } + finally + { + this.saving.Remove(kind); + } + } + + private async Task SaveAsync(BflResultKind kind, BflResult result) + { + this.saving.Add(kind); + try + { + var project = await this.Store.SaveResultAsync(this.ProjectId, kind, result); + await this.ResultStored.InvokeAsync(project); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException) + { + this.Logger.LogError(exception, "Could not save the result '{Kind}' of a preparation in the BeyondFacts Lab.", kind); + await this.MessageBus.SendError(new(Icons.Material.Filled.SaveAs, TA("AI Studio could not save this material. Please try again."))); + } + finally + { + this.saving.Remove(kind); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflStaleness.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflStaleness.cs index abed2c6aa..d8eb29d3e 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflStaleness.cs +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflStaleness.cs @@ -4,13 +4,13 @@ using System.Text; namespace AIStudio.Assistants.BeyondFactsLab; /// -/// Tells whether a confirmed work state of the BeyondFacts Lab still fits what it builds on. +/// Tells whether a confirmed work state or a result of the BeyondFacts Lab still fits what it builds on. /// /// /// The answer is computed whenever it is needed and never stored, so that it cannot drift from the work states. A /// confirmed work state remembers the revisions of the earlier steps and a fingerprint of the form of its step; /// comparing both with the present ones gives the answer. A changed form weighs less than a changed earlier step: -/// the person may have only corrected a typo. +/// the person may have only corrected a typo. A result remembers the revisions of the work states of all steps. /// public static class BflStaleness { @@ -60,6 +60,36 @@ public static class BflStaleness return revisions; } + /// + /// Gets the revisions of the confirmed work states of all steps, which a result remembers when the lab writes it. + /// + /// The work states of the preparation, by their steps. + /// The revision of each step with a confirmed work state. + public static IReadOnlyDictionary ConfirmedRevisionsOf(IReadOnlyDictionary workStates) + { + var revisions = new Dictionary(); + foreach (var step in BflStep.All) + { + if (workStates.GetValueOrDefault(step)?.Confirmed is { } confirmed) + revisions[step] = confirmed.Revision; + } + + return revisions; + } + + /// + /// Tells whether a version of a result no longer fits the work states: a step was confirmed for the first time, + /// confirmed anew, or withdrawn since the lab wrote it. + /// + /// The version of the result. + /// The work states of the preparation, by their steps. + /// True, when the result is outdated. + public static bool IsOutdated(BflResultVersion version, IReadOnlyDictionary workStates) + { + var revisions = ConfirmedRevisionsOf(workStates); + return revisions.Count != version.WorkStateRevisions.Count || revisions.Any(revision => version.WorkStateRevisions.GetValueOrDefault(revision.Key) != revision.Value); + } + /// /// Computes the fingerprint of a form, which tells whether the person changed it. /// diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflWorkStateGenerator.cs b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflWorkStateGenerator.cs index 3761e0619..84dac0084 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflWorkStateGenerator.cs +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/BflWorkStateGenerator.cs @@ -1,4 +1,3 @@ -using AIStudio.Chat; using AIStudio.Provider; using AIStudio.Tools.AssistantSessions; @@ -93,11 +92,11 @@ public sealed class BflWorkStateGenerator(BeyondFactsLabStore store, BflConversa sessionId = (await sessionService.TryBeginAsync(sessionKey, Tools.Components.BEYOND_FACTS_LAB_ASSISTANT.Name, cancellation, null, new(StringComparer.Ordinal))).SessionId; await this.NotifyAsync(location); - var answer = BflMarkdownSections.Read(await AskAsync(hidden, provider, request, requestMaterial.Images, cancellation.Token), location.Step, form); + var answer = BflMarkdownSections.Read(await BflHiddenRequest.AskAsync(hidden, provider, request, requestMaterial.Images, cancellation.Token), location.Step, form); if (answer.Problems.Count > 0 && !cancellation.IsCancellationRequested) { logger.LogInformation("The model wrote a work state of the BeyondFacts Lab with {Count} problems; it may correct them once.", answer.Problems.Count); - answer = BflMarkdownSections.Read(await AskAsync(hidden, provider, BflPromptComposer.ComposeWorkStateRepairRequest(answer.Problems), requestMaterial.Images, cancellation.Token), location.Step, form); + answer = BflMarkdownSections.Read(await BflHiddenRequest.AskAsync(hidden, provider, BflPromptComposer.ComposeWorkStateRepairRequest(answer.Problems), requestMaterial.Images, cancellation.Token), location.Step, form); } if (cancellation.IsCancellationRequested) @@ -164,41 +163,6 @@ public sealed class BflWorkStateGenerator(BeyondFactsLabStore store, BflConversa /// The step. public Task CancelAsync(BflStepThreadLocation location) => sessionService.CancelAsync(SessionKey(location)); - /// - /// Sends a request in the hidden copy of a conversation and waits for the whole answer. - /// - /// The hidden copy of the conversation, which keeps the request and the answer. - /// The provider which answers. - /// The request. - /// The images of the material, which go along with the first message. - /// The cancellation token. - /// The answer, empty when the provider failed. - private static async Task AskAsync(ChatThread hidden, AIStudio.Settings.Provider provider, string text, IReadOnlyList images, CancellationToken token) - { - var request = new ContentText { Text = text }; - hidden.Blocks.Add(new ContentBlock - { - Time = DateTimeOffset.Now, - ContentType = ContentType.TEXT, - Role = ChatRole.USER, - Content = request, - HideFromUser = true - }); - - var answer = new ContentText { InitialRemoteWait = true }; - hidden.Blocks.Add(new ContentBlock - { - Time = DateTimeOffset.Now, - ContentType = ContentType.TEXT, - Role = ChatRole.AI, - Content = answer, - HideFromUser = true - }); - - await answer.CreateFromProviderAsync(provider.CreateProvider(), provider.Model, request, BflConversationService.WithImages(hidden, images), token); - return answer.Text; - } - /// /// Tells every page which listens that a work state changed. /// diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/onboarding.md b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/onboarding.md index 43aef7c03..0f02da3d5 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/onboarding.md +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/onboarding.md @@ -41,6 +41,7 @@ Für schnelle Texte wie eine Pressemitteilung passen der Chat von AI Studio oder - Im Bereich „Material“ neben den Schritten kann die Person Dokumente hinzufügen, etwa Notizen, einen Vortragstext, ein Paper oder die Einladung, außerdem Audio- und Videoaufnahmen, etwa ein Interview, und Bilder, etwa ein Poster. Aufnahmen lässt das Lab vom Transkriptionsanbieter von AI Studio verschriftlichen; Bilder sieht nur ein Modell, das Bilder verarbeiten kann. Das Material gilt für alle Schritte: Das Lab liest es in jedem Gespräch mit und fragt danach. Reicht der Kontext des Modells nicht für alles, liest es nur den Anfang eines Materials und zeigt das an. - Mit „Gespräch beginnen“ eröffnet das Lab das Gespräch eines Schritts. Die Person kann auch selbst zuerst schreiben und sagen, wobei sie Unterstützung möchte, etwa bei ihrer Rolle, ihrer Strategie oder einer kritischen Fragerunde. - Unter dem Gespräch eines Schritts erzeugt das Lab auf Wunsch einen Arbeitsstand: eine Zusammenfassung dessen, was die Person in dem Schritt geklärt hat. Sie prüft, bearbeitet und bestätigt ihn; die späteren Schritte bauen auf den bestätigten Arbeitsständen auf. Dabei schlägt das Lab Werte für leere Felder in „Ihre Angaben“ vor, die die Person einzeln übernimmt. +- Im letzten Schritt erstellt das Lab aus den bestätigten Arbeitsständen fünf Materialien: eine Reflexionsmappe, ein Positionsblatt, einen Gesprächsleitfaden, Fragen und Antwortansätze sowie offene Punkte und nächste Schritte. Die Person prüft, bearbeitet und bestätigt jedes davon und exportiert es als Dokument. Persönliche Angaben nimmt die Reflexionsmappe nur auf, wenn die Person es wählt. - Die Schritte bauen aufeinander auf, die Person kann sie aber in beliebiger Reihenfolge bearbeiten. - Eine Vorbereitung dauert etwa 30 bis 60 Minuten. Die Person kann jederzeit unterbrechen: AI Studio speichert ihre Angaben und Gespräche, und sie macht später dort weiter. - Für jede Vorbereitung wählt die Person einen KI-Anbieter. Das Lab bietet nur Anbieter an, denen sie oder ihre Organisation genug vertraut, und das Modell muss mindestens 32.000 Token auf einmal lesen können. diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/conversation-guide.md b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/conversation-guide.md new file mode 100644 index 000000000..703826042 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/conversation-guide.md @@ -0,0 +1,23 @@ +Schreibe jetzt den Gesprächsleitfaden der Person: was sie bei ihrem Auftritt sagen und fragen kann, in Formulierungen, die sie so aussprechen kann. Die Person prüft und bearbeitet ihn, bevor sie ihn bestätigt und exportiert. + +# Grundlage + +- Stütze dich auf die bestätigten Arbeitsstände, die Angaben der Person, ihr Material und dieses Gespräch. Erfinde keine Positionen, Argumente, Belege, Zahlen oder Quellen dazu. +- Richte den Leitfaden nach dem Format der Veranstaltung, ihrem Publikum und der Redezeit aus den Angaben, etwa für einen Vortrag, einen Pitch, eine Anhörung oder ein Podium. Fehlen diese Angaben, schreibe ihn für ein Podium und sage das in einem Satz unter der Überschrift. +- Persönliche Angaben, etwa wie das Thema die Person persönlich berührt, gehören nicht hinein, solange die Person sie nicht selbst als Teil ihres Auftritts bestätigt hat. +- Für diese Schritte gibt es noch keinen bestätigten Arbeitsstand: {{MISSING_WORK_STATES}} Fülle ihre Themen nicht mit Vermutungen. Fehlt dadurch ein Teil des Leitfadens, schreibe dort kurz, dass er noch offen ist. + +# Inhalt + +1. Einstieg: zwei bis drei Sätze, mit denen die Person beginnt. +2. Zentrale Botschaften: drei, jede in einem Satz, dazu ein Beispiel oder Beleg und ein Übergang zur nächsten. +3. Fragen an andere: Fragen an andere Teilnehmende oder an das Publikum, die das Gespräch voranbringen. +4. Formulierungen für Unsicherheit: wie die Person sagt, was unsicher oder unbekannt ist, ohne Vertrauen in die Wissenschaft zu untergraben. +5. Formulierungen für Meinungsverschiedenheiten: wie die Person widerspricht oder eine Wertabwägung offenlegt, sachlich und respektvoll. +6. Abschluss: ein bis zwei Sätze, mit denen die Person endet. + +# Form + +- Beginne mit einer Überschrift erster Ebene, die den Leitfaden benennt, und gliedere die Teile mit Überschriften zweiter Ebene. +- Schreibe die Formulierungen in der ersten Person, so wie die Person sie sagen würde, ohne Anrede an die Person und in der Sprache des Gesprächs. +- Antworte nur mit dem Leitfaden in Markdown, ohne Einleitung und ohne Nachsatz, und setze ihn nicht in einen Codeblock. \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/open-points.md b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/open-points.md new file mode 100644 index 000000000..f5a10eeaa --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/open-points.md @@ -0,0 +1,21 @@ +Schreibe jetzt die offenen Punkte und nächsten Schritte der Person: was sie vor ihrem Auftritt noch recherchieren, mit anderen klären oder selbst entscheiden muss. Die Person prüft und bearbeitet die Liste, bevor sie sie bestätigt und exportiert. + +# Grundlage + +- Sammle die offenen Punkte aus allen bestätigten Arbeitsständen, dazu, was die Person im letzten Schritt offen gelassen oder als nächsten Schritt genannt hat, und was in diesem Gespräch offen geblieben ist. Führe gleiche Punkte zusammen. Erfinde keine Aufgaben dazu. +- Für diese Schritte gibt es noch keinen bestätigten Arbeitsstand: {{MISSING_WORK_STATES}} Nimm jeden von ihnen als eigenen Punkt auf: Die Person sollte ihn noch durcharbeiten oder seinen Arbeitsstand bestätigen. +- Persönliche Angaben, etwa wie das Thema die Person persönlich berührt, gehören nicht hinein. + +# Inhalt + +1. Recherchieren: was die Person noch nachlesen, prüfen oder belegen muss. +2. Mit anderen klären: was sie mit wem klären muss, etwa mit den Veranstaltenden, der Pressestelle oder Kolleginnen und Kollegen. Nenne die Ansprechpersonen nur, wenn die Person sie genannt hat. +3. Selbst entscheiden: was die Person für sich entscheiden muss, etwa eine Wertabwägung oder ihre Rolle. + +Ordne die Punkte in jedem Teil danach, was vor dem Auftritt am dringendsten ist. Bleibt ein Teil leer, schreibe das in einem Satz. + +# Form + +- Beginne mit einer Überschrift erster Ebene, die die Liste benennt, und gliedere die drei Teile mit Überschriften zweiter Ebene. +- Schreibe jeden Punkt als Aufgabe zum Abhaken, also als Zeile mit „- [ ] “, knapp, ohne Anrede an die Person und in der Sprache des Gesprächs. +- Antworte nur mit der Liste in Markdown, ohne Einleitung und ohne Nachsatz, und setze sie nicht in einen Codeblock. \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/position-paper.md b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/position-paper.md new file mode 100644 index 000000000..17166720d --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/position-paper.md @@ -0,0 +1,23 @@ +Schreibe jetzt das Positionsblatt der Person: eine Seite, die sie vor ihrem Auftritt liest und auf die sie währenddessen schauen kann. Es hält fest, was sie vertritt und warum. Die Person prüft und bearbeitet es, bevor sie es bestätigt und exportiert. + +# Grundlage + +- Stütze dich auf die bestätigten Arbeitsstände, die Angaben der Person, ihr Material und dieses Gespräch. Erfinde keine Positionen, Argumente, Belege, Zahlen oder Quellen dazu. +- Übernimm Deutungen, Werte oder Motive nur, wenn die Person sie selbst bestätigt hat. +- Persönliche Angaben, etwa wie das Thema die Person persönlich berührt, gehören nicht hinein: Die Person kann das Blatt weitergeben. +- Für diese Schritte gibt es noch keinen bestätigten Arbeitsstand: {{MISSING_WORK_STATES}} Fülle ihre Themen nicht mit Vermutungen. Fehlt dadurch ein Teil des Blatts, schreibe dort kurz, dass er noch offen ist. + +# Inhalt + +1. Kernposition: ein bis zwei Sätze, was die Person vertritt. +2. Zentrale Aussagen: drei bis fünf, jede mit ihrer Begründung und, wo vorhanden, dem Beleg oder dem Material, auf das sie sich stützt. +3. Wertabwägungen: welche Werte die Position tragen und wo die Person zwischen ihnen abwägt. +4. Grenzen der eigenen Expertise: was die Person nicht beurteilen kann, was unsicher ist und was sie bewusst offen lässt. +5. Rolle: in welcher Rolle die Person spricht und was sie mit ihrem Beitrag erreichen möchte. + +# Form + +- Höchstens etwa 400 Wörter, damit das Blatt auf eine Seite passt. +- Beginne mit einer Überschrift erster Ebene, die das Blatt als Positionsblatt benennt, und gliedere die Teile mit Überschriften zweiter Ebene. +- Schreibe aus der Sicht der Person in der ersten Person, knapp, in Stichpunkten oder kurzen Sätzen, ohne Anrede an die Person und in der Sprache des Gesprächs. +- Antworte nur mit dem Positionsblatt in Markdown, ohne Einleitung und ohne Nachsatz, und setze es nicht in einen Codeblock. \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/questions-and-answers.md b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/questions-and-answers.md new file mode 100644 index 000000000..25546b815 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/results/questions-and-answers.md @@ -0,0 +1,19 @@ +Schreibe jetzt die Fragen und Antwortansätze der Person: die kritischen Nachfragen, die ihr bei ihrem Auftritt begegnen können, und wie sie darauf antworten kann. Die Person prüft und bearbeitet sie, bevor sie sie bestätigt und exportiert. + +# Grundlage + +- Die Fragen kommen vor allem aus der Erprobung in der Testsituation. Hat die Testsituation keinen bestätigten Arbeitsstand, leite wahrscheinliche Fragen aus den übrigen Arbeitsständen ab und schreibe unter die Überschrift, dass diese Fragen noch nicht erprobt sind. +- Die Antwortansätze stützen sich auf die bestätigten Arbeitsstände, die Angaben der Person, ihr Material und dieses Gespräch. Erfinde keine Positionen, Argumente, Belege, Zahlen oder Quellen dazu. +- Persönliche Angaben, etwa wie das Thema die Person persönlich berührt, gehören nicht hinein, solange die Person sie nicht selbst als Teil ihrer Antwort bestätigt hat. +- Für diese Schritte gibt es noch keinen bestätigten Arbeitsstand: {{MISSING_WORK_STATES}} Fülle ihre Themen nicht mit Vermutungen. + +# Inhalt + +1. Kritische Fragen mit Antwortansatz: fünf bis acht Fragen. Zu jeder: die Frage so, wie sie gestellt werden könnte; die Kernantwort in ein bis zwei Sätzen; worauf die Antwort sich stützt und was an ihr unsicher ist; wo die Person ihre Grenze zieht. +2. Ohne belastbare Antwort: die Punkte, bei denen die Person ausdrücklich keine belastbare Antwort hat, jeweils mit einer ehrlichen Formulierung dafür. Gibt es keine, schreibe das in einem Satz. + +# Form + +- Beginne mit einer Überschrift erster Ebene, die das Material benennt, gliedere die beiden Teile mit Überschriften zweiter Ebene und jede Frage mit einer Überschrift dritter Ebene. +- Schreibe die Antwortansätze in der ersten Person, so wie die Person sie sagen würde, ohne Anrede an die Person und in der Sprache des Gesprächs. +- Antworte nur mit den Fragen und Antwortansätzen in Markdown, ohne Einleitung und ohne Nachsatz, und setze sie nicht in einen Codeblock. \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/steps/07-results.md b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/steps/07-results.md index 14b753847..b39378872 100644 --- a/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/steps/07-results.md +++ b/app/MindWork AI Studio/Assistants/BeyondFactsLab/Prompts/steps/07-results.md @@ -4,18 +4,20 @@ In diesem Schritt hältst du mit der Person fest, was sie aus ihrer Vorbereitung ### Deine erste Nachricht -Stelle die Leitfrage dieses Schritts: „Was haben Sie gegenüber Ihrer Ausgangsposition verändert, beibehalten oder offengelassen, und weshalb?“ Hat die Person sie in den Angaben schon beantwortet, greife ihre Antwort auf und frage nach, wo es sich lohnt. Nenne danach kurz die Materialien, die du erstellen kannst. +Stelle die Leitfrage dieses Schritts: „Was haben Sie gegenüber Ihrer Ausgangsposition verändert, beibehalten oder offengelassen, und weshalb?“ Hat die Person sie in den Angaben schon beantwortet, greife ihre Antwort auf und frage nach, wo es sich lohnt. Nenne danach kurz die Materialien, die das Lab unter diesem Gespräch für sie erstellt. ### Materialien -Erstelle sie nur auf Wunsch, eines nach dem anderen, mit dem, was die Angaben, das Material der Person und das Gespräch enthalten. Benenne, was dafür noch fehlt, statt es zu erfinden. +Unter diesem Gespräch erstellt das Lab fünf Materialien. Die Person prüft, bearbeitet und bestätigt jedes davon und exportiert es als Dokument: -- Reflexionsmappe: die Ergebnisse aller Schritte, Quellen, darunter das Material der Person, offene Fragen und Überarbeitungen. +- Reflexionsmappe: die bestätigten Arbeitsstände aller Schritte, das Material der Person, offene Fragen und Überarbeitungen. AI Studio setzt sie wörtlich zusammen, ohne Modell. - Positionsblatt, höchstens eine Seite: Was vertrete ich und warum? Zentrale Aussagen, Begründungen, Wertabwägungen und die Grenzen der eigenen Expertise. - Gesprächsleitfaden: Einstieg, zentrale Botschaften, Fragen an andere und Formulierungen für Unsicherheit oder Meinungsverschiedenheiten; anpassbar für Vortrag, Pitch oder Anhörung. - Fragen und Antwortansätze: kritische Nachfragen, eigene Antwortansätze und Punkte, auf die die Person ausdrücklich keine belastbare Antwort hat. - Offene Punkte und nächste Schritte: Was muss die Person vor dem Auftritt recherchieren, mit anderen klären oder selbst entscheiden? +Diese Materialien schreibst du nicht im Gespräch. Fragt die Person danach, verweise sie auf den Bereich unter dem Gespräch. Die Materialien bauen auf den bestätigten Arbeitsständen auf, und was ihr in diesem Gespräch klärt, fließt in die vier Materialien ein, die das Modell schreibt. Fehlen Arbeitsstände, rate der Person, sie vorher zu bestätigen, auch den Arbeitsstand dieses Schritts. + Auf Wunsch erstellst du auch kleinere Hilfen: - Karteikarten: drei bis sieben Karten, je mit einer Kernbotschaft, einem Beleg oder Beispiel und einem Übergang. diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index a7efb9a65..5812e0d1e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1255,6 +1255,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1231621 -- The lab writes no press releases or other PR texts, and it checks no facts. It replaces neither your own expert review nor the legal and data protection advice of your institution, and it makes no political or normative decision for you. For a quick text, use the chat or another assistant of AI Studio. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1231621082::INFORMAL"] = "The lab writes no press releases or other PR texts, and it checks no facts. It replaces neither your own expert review nor the legal and data protection advice of your institution, and it makes no political or normative decision for you. For a quick text, use the chat or another assistant of AI Studio." +-- In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1266652635"] = "In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document." + +-- In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1266652635::INFORMAL"] = "In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document." + -- You keep the decision about which position you take. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1509612483"] = "You keep the decision about which position you take." @@ -1504,6 +1510,240 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE:: -- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521::INFORMAL"] = "The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab." +-- No confirmed work state. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1085223273"] = "No confirmed work state." + +-- The details of this step changed after this confirmation. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T116735836"] = "The details of this step changed after this confirmation." + +-- Appendix: the conversations +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1392283909"] = "Appendix: the conversations" + +-- Image +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1494001562"] = "Image" + +-- No confirmed work state names open questions. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1524103624"] = "No confirmed work state names open questions." + +-- Open questions +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T2962485044"] = "Open questions" + +-- You +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3049115964"] = "You" + +-- You +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3049115964::INFORMAL"] = "You" + +-- No material was added. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3104633786"] = "No material was added." + +-- Lab +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3303746196"] = "Lab" + +-- Material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3419754368"] = "Material" + +-- Recording +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3881628172"] = "Recording" + +-- Version {0}, confirmed on {1}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3926589642"] = "Version {0}, confirmed on {1}." + +-- Left out, as it may contain personal disclosures. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3986361996"] = "Left out, as it may contain personal disclosures." + +-- Document +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T4165204724"] = "Document" + +-- An earlier step changed after this confirmation. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T4271561391"] = "An earlier step changed after this confirmation." + +-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1150375027"] = "The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again." + +-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1150375027::INFORMAL"] = "The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again." + +-- AI Studio could not create this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1258513697"] = "AI Studio could not create this material. Please try again." + +-- AI Studio could not create this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1258513697::INFORMAL"] = "AI Studio could not create this material. Please try again." + +-- The model did not write this material. Please try again, or try another provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T2403507896"] = "The model did not write this material. Please try again, or try another provider." + +-- The model did not write this material. Please try again, or try another provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T2403507896::INFORMAL"] = "The model did not write this material. Please try again, or try another provider." + +-- Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T1198530903"] = "Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer." + +-- Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T1198530903::INFORMAL"] = "Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer." + +-- The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3304526288"] = "The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing." + +-- The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3304526288::INFORMAL"] = "The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing." + +-- Reflection folder +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3525062904"] = "Reflection folder" + +-- All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3690314691"] = "All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix." + +-- All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3690314691::INFORMAL"] = "All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix." + +-- Open points and next steps +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3873392021"] = "Open points and next steps" + +-- Position paper +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3950911696"] = "Position paper" + +-- One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4085659087"] = "One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise." + +-- One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4085659087::INFORMAL"] = "One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise." + +-- Conversation guide +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4171886338"] = "Conversation guide" + +-- Questions and answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T704553916"] = "Questions and answers" + +-- What you still have to research, clarify with others, or decide yourself before you appear. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T83625072"] = "What you still have to research, clarify with others, or decide yourself before you appear." + +-- What you still have to research, clarify with others, or decide yourself before you appear. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T83625072::INFORMAL"] = "What you still have to research, clarify with others, or decide yourself before you appear." + +-- The lab writes this material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1161855660"] = "The lab writes this material" + +-- Stop +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1266644741"] = "Stop" + +-- You confirmed this material on {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1463626379"] = "You confirmed this material on {0}." + +-- You confirmed this material on {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1463626379::INFORMAL"] = "You confirmed this material on {0}." + +-- AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1470884556"] = "AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged." + +-- AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1470884556::INFORMAL"] = "AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged." + +-- Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1551694648"] = "Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts." + +-- Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1551694648::INFORMAL"] = "Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts." + +-- A work state changed since the lab wrote this draft. You can create it anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1597401023"] = "A work state changed since the lab wrote this draft. You can create it anew." + +-- A work state changed since the lab wrote this draft. You can create it anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1597401023::INFORMAL"] = "A work state changed since the lab wrote this draft. You can create it anew." + +-- The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T182456751"] = "The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document." + +-- The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T182456751::INFORMAL"] = "The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document." + +-- AI Studio could not export this material. Please try another format. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T195200475"] = "AI Studio could not export this material. Please try another format." + +-- AI Studio could not export this material. Please try another format. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T195200475::INFORMAL"] = "AI Studio could not export this material. Please try another format." + +-- Cannot be opened +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1981873292"] = "Cannot be opened" + +-- These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2174778813"] = "These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open." + +-- These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2174778813::INFORMAL"] = "These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open." + +-- Discard draft +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2273967454"] = "Discard draft" + +-- Draft +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2313908524"] = "Draft" + +-- What would you like to take away? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2505178919"] = "What would you like to take away?" + +-- What would you like to take away? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2505178919::INFORMAL"] = "What would you like to take away?" + +-- Draft, not confirmed yet +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2553562678"] = "Draft, not confirmed yet" + +-- Create +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2567824509"] = "Create" + +-- Not created yet +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2679230892"] = "Not created yet" + +-- Confirm +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2709768687"] = "Confirm" + +-- A work state changed since you confirmed this material. Please create it anew, or check whether it still fits. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2881263992"] = "A work state changed since you confirmed this material. Please create it anew, or check whether it still fits." + +-- A work state changed since you confirmed this material. Please create it anew, or check whether it still fits. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2881263992::INFORMAL"] = "A work state changed since you confirmed this material. Please create it anew, or check whether it still fits." + +-- Add the conversations as an appendix +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3185544181"] = "Add the conversations as an appendix" + +-- Add the conversations as an appendix +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3185544181::INFORMAL"] = "Add the conversations as an appendix" + +-- Being written +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3204128327"] = "Being written" + +-- Edit +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3267849393"] = "Edit" + +-- AI Studio could not save this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3460571754"] = "AI Studio could not save this material. Please try again." + +-- AI Studio could not save this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3460571754::INFORMAL"] = "AI Studio could not save this material. Please try again." + +-- Create anew +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3560413436"] = "Create anew" + +-- Export +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3898821075"] = "Export" + +-- Create all materials in parallel +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3926195314"] = "Create all materials in parallel" + +-- Include personal disclosures +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T4086721095"] = "Include personal disclosures" + +-- Include personal disclosures +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T4086721095::INFORMAL"] = "Include personal disclosures" + +-- Outdated +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T724820205"] = "Outdated" + +-- Show preview +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T934952176"] = "Show preview" + +-- Confirmed +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T942512558"] = "Confirmed" + -- Test situation UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Test situation" @@ -12385,9 +12625,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T613888204"] = "Translation" -- Rewrite and improve a given text for a chosen style. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T722167136"] = "Rewrite and improve a given text for a chosen style." --- Learning -UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T755590027"] = "Learning" - -- Bias of the Day UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T782102948"] = "Bias of the Day" @@ -12403,6 +12640,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T892147062"] = "View and filter AI -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization" +-- Learning & Reflecting +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T912918956"] = "Learning & Reflecting" + -- Turn texts and documents into slides, also as a PowerPoint file in the design of your template. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T943132958"] = "Turn texts and documents into slides, also as a PowerPoint file in the design of your template." @@ -14221,8 +14461,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T3802462536"] = " -- Select the assistant category UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T4193824894"] = "Select the assistant category" --- Learning -UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T755590027"] = "Learning" +-- Learning & Reflecting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T912918956"] = "Learning & Reflecting" -- This assistant is already running. AI Studio opens the running session instead. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTLAUNCH::ASSISTANTLAUNCHSERVICE::T2575715765"] = "This assistant is already running. AI Studio opens the running session instead." diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index f14838102..b497f8d79 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -68,7 +68,7 @@ - + diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index c5ab0bd2b..13caf8a73 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1257,6 +1257,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1231621 -- The lab writes no press releases or other PR texts, and it checks no facts. It replaces neither your own expert review nor the legal and data protection advice of your institution, and it makes no political or normative decision for you. For a quick text, use the chat or another assistant of AI Studio. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1231621082::INFORMAL"] = "Das Lab schreibt keine Pressemitteilungen oder anderen PR-Texte und prüft keine Fakten. Es ersetzt weder deine eigene fachliche Prüfung noch die Rechts- und Datenschutzberatung deiner Institution, und es nimmt dir keine politische oder normative Entscheidung ab. Für einen schnellen Text nutze den Chat oder einen anderen Assistenten von AI Studio." +-- In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1266652635"] = "Lassen Sie im letzten Schritt das Lab aus Ihren bestätigten Arbeitsständen Ihre Materialien erstellen: eine Reflexionsmappe, ein Positionsblatt, einen Gesprächsleitfaden, Fragen und Antwortansätze sowie Ihre offenen Punkte. Sie prüfen, bearbeiten und bestätigen jedes davon und exportieren es dann als Dokument." + +-- In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1266652635::INFORMAL"] = "Lass im letzten Schritt das Lab aus deinen bestätigten Arbeitsständen deine Materialien erstellen: eine Reflexionsmappe, ein Positionsblatt, einen Gesprächsleitfaden, Fragen und Antwortansätze sowie deine offenen Punkte. Du prüfst, bearbeitest und bestätigst jedes davon und exportierst es dann als Dokument." + -- You keep the decision about which position you take. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1509612483"] = "Sie behalten die Entscheidung darüber, welche Position Sie vertreten." @@ -1506,6 +1512,240 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE:: -- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521::INFORMAL"] = "Das Lab verwendet derzeit experimentelle Anweisungen aus seinem Testordner. Du kannst sie in den Optionen des Labs deaktivieren." +-- No confirmed work state. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1085223273"] = "Kein bestätigter Arbeitsstand." + +-- The details of this step changed after this confirmation. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T116735836"] = "Die Angaben dieses Schritts haben sich nach dieser Bestätigung geändert." + +-- Appendix: the conversations +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1392283909"] = "Anhang: die Gespräche" + +-- Image +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1494001562"] = "Bild" + +-- No confirmed work state names open questions. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1524103624"] = "Kein bestätigter Arbeitsstand nennt offene Fragen." + +-- Open questions +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T2962485044"] = "Offene Fragen" + +-- You +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3049115964"] = "Sie" + +-- You +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3049115964::INFORMAL"] = "Du" + +-- No material was added. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3104633786"] = "Es wurde kein Material hinzugefügt." + +-- Lab +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3303746196"] = "Lab" + +-- Material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3419754368"] = "Material" + +-- Recording +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3881628172"] = "Aufnahme" + +-- Version {0}, confirmed on {1}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3926589642"] = "Version {0}, bestätigt am {1}." + +-- Left out, as it may contain personal disclosures. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3986361996"] = "Ausgelassen, da es persönliche Angaben enthalten kann." + +-- Document +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T4165204724"] = "Dokument" + +-- An earlier step changed after this confirmation. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T4271561391"] = "Ein früherer Schritt hat sich nach dieser Bestätigung geändert." + +-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1150375027"] = "Die Datei „{0}“ im Testordner des BeyondFacts Labs enthält den Platzhalter „{1}“, den das Lab in dieser Datei nicht ausfüllt. Bitte korrigieren Sie die Datei und versuchen Sie es erneut." + +-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1150375027::INFORMAL"] = "Die Datei „{0}“ im Testordner des BeyondFacts Labs enthält den Platzhalter „{1}“, den das Lab in dieser Datei nicht ausfüllt. Bitte korrigiere die Datei und versuche es erneut." + +-- AI Studio could not create this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1258513697"] = "AI Studio konnte dieses Material nicht erstellen. Bitte versuchen Sie es erneut." + +-- AI Studio could not create this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1258513697::INFORMAL"] = "AI Studio konnte dieses Material nicht erstellen. Bitte versuche es erneut." + +-- The model did not write this material. Please try again, or try another provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T2403507896"] = "Das Modell hat dieses Material nicht geschrieben. Bitte versuchen Sie es erneut, oder wählen Sie einen anderen Anbieter aus." + +-- The model did not write this material. Please try again, or try another provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T2403507896::INFORMAL"] = "Das Modell hat dieses Material nicht geschrieben. Bitte versuche es erneut, oder wähle einen anderen Anbieter aus." + +-- Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T1198530903"] = "Kritische Nachfragen aus der Testsituation, Ihre Ansätze, sie zu beantworten, und die Punkte, auf die Sie keine belastbare Antwort haben." + +-- Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T1198530903::INFORMAL"] = "Kritische Nachfragen aus der Testsituation, deine Ansätze, sie zu beantworten, und die Punkte, auf die du keine belastbare Antwort hast." + +-- The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3304526288"] = "Der Einstieg, Ihre zentralen Botschaften, Fragen an andere und Formulierungen für Unsicherheit oder Meinungsverschiedenheiten. Sie können ihn für einen Vortrag, einen Pitch oder eine Anhörung anpassen." + +-- The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3304526288::INFORMAL"] = "Der Einstieg, deine zentralen Botschaften, Fragen an andere und Formulierungen für Unsicherheit oder Meinungsverschiedenheiten. Du kannst ihn für einen Vortrag, einen Pitch oder eine Anhörung anpassen." + +-- Reflection folder +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3525062904"] = "Reflexionsmappe" + +-- All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3690314691"] = "Alle Ihre bestätigten Arbeitsstände, Ihr Material, die offenen Fragen und Ihre Überarbeitungen, wörtlich. Auf Wunsch mit den vollständigen Gesprächen als Anhang." + +-- All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3690314691::INFORMAL"] = "Alle deine bestätigten Arbeitsstände, dein Material, die offenen Fragen und deine Überarbeitungen, wörtlich. Auf Wunsch mit den vollständigen Gesprächen als Anhang." + +-- Open points and next steps +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3873392021"] = "Offene Punkte und nächste Schritte" + +-- Position paper +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3950911696"] = "Positionsblatt" + +-- One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4085659087"] = "Eine Seite dazu, was Sie vertreten und warum: Ihre zentralen Aussagen, Ihre Begründungen, die Abwägungen zwischen Werten und die Grenzen Ihrer Expertise." + +-- One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4085659087::INFORMAL"] = "Eine Seite dazu, was du vertrittst und warum: deine zentralen Aussagen, deine Begründungen, die Abwägungen zwischen Werten und die Grenzen deiner Expertise." + +-- Conversation guide +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4171886338"] = "Gesprächsleitfaden" + +-- Questions and answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T704553916"] = "Fragen und Antwortansätze" + +-- What you still have to research, clarify with others, or decide yourself before you appear. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T83625072"] = "Was Sie vor Ihrem Auftritt noch recherchieren, mit anderen klären oder selbst entscheiden müssen." + +-- What you still have to research, clarify with others, or decide yourself before you appear. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T83625072::INFORMAL"] = "Was du vor deinem Auftritt noch recherchieren, mit anderen klären oder selbst entscheiden musst." + +-- The lab writes this material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1161855660"] = "Das Lab schreibt dieses Material" + +-- Stop +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1266644741"] = "Stoppen" + +-- You confirmed this material on {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1463626379"] = "Sie haben dieses Material am {0} bestätigt." + +-- You confirmed this material on {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1463626379::INFORMAL"] = "Du hast dieses Material am {0} bestätigt." + +-- AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1470884556"] = "AI Studio kann dieses Material nicht öffnen. Seine Datei ist möglicherweise beschädigt oder wurde mit einer neueren Version von AI Studio gespeichert. AI Studio lässt die Datei unverändert." + +-- AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1470884556::INFORMAL"] = "AI Studio kann dieses Material nicht öffnen. Seine Datei ist möglicherweise beschädigt oder wurde mit einer neueren Version von AI Studio gespeichert. AI Studio lässt die Datei unverändert." + +-- Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1551694648"] = "Möchten Sie die noch nicht bestätigten Entwürfe ersetzen? Bestätigte Materialien bleiben erhalten, bis Sie die neuen Entwürfe bestätigen." + +-- Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1551694648::INFORMAL"] = "Möchtest du die noch nicht bestätigten Entwürfe ersetzen? Bestätigte Materialien bleiben erhalten, bis du die neuen Entwürfe bestätigst." + +-- A work state changed since the lab wrote this draft. You can create it anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1597401023"] = "Seit das Lab diesen Entwurf geschrieben hat, hat sich ein Arbeitsstand geändert. Sie können ihn neu erstellen." + +-- A work state changed since the lab wrote this draft. You can create it anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1597401023::INFORMAL"] = "Seit das Lab diesen Entwurf geschrieben hat, hat sich ein Arbeitsstand geändert. Du kannst ihn neu erstellen." + +-- The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T182456751"] = "Das Lab schreibt diese Materialien aus Ihren bestätigten Arbeitsständen. Prüfen und bearbeiten Sie jedes davon, und bestätigen Sie es. Danach können Sie es als Dokument exportieren." + +-- The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T182456751::INFORMAL"] = "Das Lab schreibt diese Materialien aus deinen bestätigten Arbeitsständen. Prüfe und bearbeite jedes davon, und bestätige es. Danach kannst du es als Dokument exportieren." + +-- AI Studio could not export this material. Please try another format. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T195200475"] = "AI Studio konnte dieses Material nicht exportieren. Bitte versuchen Sie es mit einem anderen Format." + +-- AI Studio could not export this material. Please try another format. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T195200475::INFORMAL"] = "AI Studio konnte dieses Material nicht exportieren. Bitte versuche es mit einem anderen Format." + +-- Cannot be opened +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1981873292"] = "Kann nicht geöffnet werden" + +-- These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2174778813"] = "Diese Schritte haben noch keinen bestätigten Arbeitsstand: {0}. Die Materialien verwenden nur, was Sie bestätigt haben, und nennen, was noch offen ist." + +-- These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2174778813::INFORMAL"] = "Diese Schritte haben noch keinen bestätigten Arbeitsstand: {0}. Die Materialien verwenden nur, was du bestätigt hast, und nennen, was noch offen ist." + +-- Discard draft +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2273967454"] = "Entwurf verwerfen" + +-- Draft +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2313908524"] = "Entwurf" + +-- What would you like to take away? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2505178919"] = "Was möchten Sie mitnehmen?" + +-- What would you like to take away? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2505178919::INFORMAL"] = "Was möchtest du mitnehmen?" + +-- Draft, not confirmed yet +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2553562678"] = "Entwurf, noch nicht bestätigt" + +-- Create +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2567824509"] = "Erstellen" + +-- Not created yet +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2679230892"] = "Noch nicht erstellt" + +-- Confirm +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2709768687"] = "Bestätigen" + +-- A work state changed since you confirmed this material. Please create it anew, or check whether it still fits. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2881263992"] = "Seit Sie dieses Material bestätigt haben, hat sich ein Arbeitsstand geändert. Bitte erstellen Sie es neu, oder prüfen Sie, ob es noch passt." + +-- A work state changed since you confirmed this material. Please create it anew, or check whether it still fits. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2881263992::INFORMAL"] = "Seit du dieses Material bestätigt hast, hat sich ein Arbeitsstand geändert. Bitte erstelle es neu, oder prüfe, ob es noch passt." + +-- Add the conversations as an appendix +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3185544181"] = "Gespräche als Anhang aufnehmen" + +-- Add the conversations as an appendix +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3185544181::INFORMAL"] = "Gespräche als Anhang aufnehmen" + +-- Being written +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3204128327"] = "Wird geschrieben" + +-- Edit +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3267849393"] = "Bearbeiten" + +-- AI Studio could not save this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3460571754"] = "AI Studio konnte dieses Material nicht speichern. Bitte versuchen Sie es erneut." + +-- AI Studio could not save this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3460571754::INFORMAL"] = "AI Studio konnte dieses Material nicht speichern. Bitte versuche es erneut." + +-- Create anew +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3560413436"] = "Neu erstellen" + +-- Export +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3898821075"] = "Exportieren" + +-- Create all materials in parallel +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3926195314"] = "Alle Materialien parallel erstellen" + +-- Include personal disclosures +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T4086721095"] = "Persönliche Angaben aufnehmen" + +-- Include personal disclosures +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T4086721095::INFORMAL"] = "Persönliche Angaben aufnehmen" + +-- Outdated +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T724820205"] = "Veraltet" + +-- Show preview +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T934952176"] = "Vorschau anzeigen" + +-- Confirmed +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T942512558"] = "Bestätigt" + -- Test situation UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Testsituation" @@ -12387,9 +12627,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T613888204"] = "Übersetzung" -- Rewrite and improve a given text for a chosen style. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T722167136"] = "Einen gegebenen Text für einen gewählten Stil umformulieren und verbessern." --- Learning -UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T755590027"] = "Lernen" - -- Bias of the Day UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T782102948"] = "Vorurteil des Tages" @@ -12405,6 +12642,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T892147062"] = "AI-Studio-Protokol -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Lokalisierung" +-- Learning & Reflecting +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T912918956"] = "Lernen & Reflektieren" + -- Turn texts and documents into slides, also as a PowerPoint file in the design of your template. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T943132958"] = "Verwandeln Sie Texte und Dokumente in Folien, auch als PowerPoint-Datei im Design Ihrer Vorlage." @@ -14223,8 +14463,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T3802462536"] = " -- Select the assistant category UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T4193824894"] = "Assistentenkategorie auswählen" --- Learning -UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T755590027"] = "Lernen" +-- Learning & Reflecting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T912918956"] = "Lernen & Reflektieren" -- This assistant is already running. AI Studio opens the running session instead. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTLAUNCH::ASSISTANTLAUNCHSERVICE::T2575715765"] = "Dieser Assistent läuft bereits. AI Studio öffnet stattdessen die laufende Sitzung." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index d7c060007..231fe4c65 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1257,6 +1257,12 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1231621 -- The lab writes no press releases or other PR texts, and it checks no facts. It replaces neither your own expert review nor the legal and data protection advice of your institution, and it makes no political or normative decision for you. For a quick text, use the chat or another assistant of AI Studio. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1231621082::INFORMAL"] = "The lab writes no press releases or other PR texts, and it checks no facts. It replaces neither your own expert review nor the legal and data protection advice of your institution, and it makes no political or normative decision for you. For a quick text, use the chat or another assistant of AI Studio." +-- In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1266652635"] = "In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document." + +-- In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1266652635::INFORMAL"] = "In the last step, let the lab create your materials from your confirmed work states: a reflection folder, a position paper, a conversation guide, questions and answers, and your open points. You check, edit, and confirm each of them, and then export it as a document." + -- You keep the decision about which position you take. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLINTRODUCTION::T1509612483"] = "You keep the decision about which position you take." @@ -1506,6 +1512,240 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE:: -- The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLPROMPTOVERRIDENOTICE::T1719028521::INFORMAL"] = "The lab currently uses experimental instructions from its test folder. You can switch them off in the options of the lab." +-- No confirmed work state. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1085223273"] = "No confirmed work state." + +-- The details of this step changed after this confirmation. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T116735836"] = "The details of this step changed after this confirmation." + +-- Appendix: the conversations +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1392283909"] = "Appendix: the conversations" + +-- Image +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1494001562"] = "Image" + +-- No confirmed work state names open questions. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T1524103624"] = "No confirmed work state names open questions." + +-- Open questions +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T2962485044"] = "Open questions" + +-- You +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3049115964"] = "You" + +-- You +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3049115964::INFORMAL"] = "You" + +-- No material was added. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3104633786"] = "No material was added." + +-- Lab +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3303746196"] = "Lab" + +-- Material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3419754368"] = "Material" + +-- Recording +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3881628172"] = "Recording" + +-- Version {0}, confirmed on {1}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3926589642"] = "Version {0}, confirmed on {1}." + +-- Left out, as it may contain personal disclosures. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T3986361996"] = "Left out, as it may contain personal disclosures." + +-- Document +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T4165204724"] = "Document" + +-- An earlier step changed after this confirmation. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLREFLECTIONFOLDER::T4271561391"] = "An earlier step changed after this confirmation." + +-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1150375027"] = "The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again." + +-- The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1150375027::INFORMAL"] = "The file '{0}' in the test folder of the BeyondFacts Lab contains the placeholder '{1}', which the lab does not fill in this file. Please correct the file and try again." + +-- AI Studio could not create this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1258513697"] = "AI Studio could not create this material. Please try again." + +-- AI Studio could not create this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T1258513697::INFORMAL"] = "AI Studio could not create this material. Please try again." + +-- The model did not write this material. Please try again, or try another provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T2403507896"] = "The model did not write this material. Please try again, or try another provider." + +-- The model did not write this material. Please try again, or try another provider. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTGENERATOR::T2403507896::INFORMAL"] = "The model did not write this material. Please try again, or try another provider." + +-- Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T1198530903"] = "Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer." + +-- Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T1198530903::INFORMAL"] = "Critical questions from the test situation, your approaches to answering them, and the points on which you have no reliable answer." + +-- The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3304526288"] = "The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing." + +-- The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3304526288::INFORMAL"] = "The opening, your key messages, questions to others, and wordings for uncertainty or disagreement. You can adapt it for a talk, a pitch, or a hearing." + +-- Reflection folder +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3525062904"] = "Reflection folder" + +-- All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3690314691"] = "All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix." + +-- All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3690314691::INFORMAL"] = "All your confirmed work states, your material, the open questions, and your revisions, word for word. If you like, with the complete conversations as an appendix." + +-- Open points and next steps +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3873392021"] = "Open points and next steps" + +-- Position paper +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T3950911696"] = "Position paper" + +-- One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4085659087"] = "One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise." + +-- One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4085659087::INFORMAL"] = "One page on what you stand for and why: your key statements, your reasons, the trade-offs between values, and the limits of your expertise." + +-- Conversation guide +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T4171886338"] = "Conversation guide" + +-- Questions and answers +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T704553916"] = "Questions and answers" + +-- What you still have to research, clarify with others, or decide yourself before you appear. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T83625072"] = "What you still have to research, clarify with others, or decide yourself before you appear." + +-- What you still have to research, clarify with others, or decide yourself before you appear. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTKINDEXTENSIONS::T83625072::INFORMAL"] = "What you still have to research, clarify with others, or decide yourself before you appear." + +-- The lab writes this material +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1161855660"] = "The lab writes this material" + +-- Stop +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1266644741"] = "Stop" + +-- You confirmed this material on {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1463626379"] = "You confirmed this material on {0}." + +-- You confirmed this material on {0}. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1463626379::INFORMAL"] = "You confirmed this material on {0}." + +-- AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1470884556"] = "AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged." + +-- AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1470884556::INFORMAL"] = "AI Studio cannot open this material. Its file may be damaged, or a newer version of AI Studio stored it. AI Studio leaves the file unchanged." + +-- Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1551694648"] = "Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts." + +-- Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1551694648::INFORMAL"] = "Replace the drafts you have not confirmed yet? Materials you confirmed stay until you confirm their new drafts." + +-- A work state changed since the lab wrote this draft. You can create it anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1597401023"] = "A work state changed since the lab wrote this draft. You can create it anew." + +-- A work state changed since the lab wrote this draft. You can create it anew. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1597401023::INFORMAL"] = "A work state changed since the lab wrote this draft. You can create it anew." + +-- The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T182456751"] = "The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document." + +-- The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T182456751::INFORMAL"] = "The lab writes these materials from your confirmed work states. Check and edit each of them, and confirm it. Then you can export it as a document." + +-- AI Studio could not export this material. Please try another format. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T195200475"] = "AI Studio could not export this material. Please try another format." + +-- AI Studio could not export this material. Please try another format. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T195200475::INFORMAL"] = "AI Studio could not export this material. Please try another format." + +-- Cannot be opened +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T1981873292"] = "Cannot be opened" + +-- These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2174778813"] = "These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open." + +-- These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2174778813::INFORMAL"] = "These steps have no confirmed work state yet: {0}. The materials use only what you confirmed and name what is still open." + +-- Discard draft +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2273967454"] = "Discard draft" + +-- Draft +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2313908524"] = "Draft" + +-- What would you like to take away? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2505178919"] = "What would you like to take away?" + +-- What would you like to take away? +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2505178919::INFORMAL"] = "What would you like to take away?" + +-- Draft, not confirmed yet +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2553562678"] = "Draft, not confirmed yet" + +-- Create +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2567824509"] = "Create" + +-- Not created yet +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2679230892"] = "Not created yet" + +-- Confirm +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2709768687"] = "Confirm" + +-- A work state changed since you confirmed this material. Please create it anew, or check whether it still fits. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2881263992"] = "A work state changed since you confirmed this material. Please create it anew, or check whether it still fits." + +-- A work state changed since you confirmed this material. Please create it anew, or check whether it still fits. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T2881263992::INFORMAL"] = "A work state changed since you confirmed this material. Please create it anew, or check whether it still fits." + +-- Add the conversations as an appendix +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3185544181"] = "Add the conversations as an appendix" + +-- Add the conversations as an appendix +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3185544181::INFORMAL"] = "Add the conversations as an appendix" + +-- Being written +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3204128327"] = "Being written" + +-- Edit +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3267849393"] = "Edit" + +-- AI Studio could not save this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3460571754"] = "AI Studio could not save this material. Please try again." + +-- AI Studio could not save this material. Please try again. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3460571754::INFORMAL"] = "AI Studio could not save this material. Please try again." + +-- Create anew +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3560413436"] = "Create anew" + +-- Export +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3898821075"] = "Export" + +-- Create all materials in parallel +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T3926195314"] = "Create all materials in parallel" + +-- Include personal disclosures +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T4086721095"] = "Include personal disclosures" + +-- Include personal disclosures +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T4086721095::INFORMAL"] = "Include personal disclosures" + +-- Outdated +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T724820205"] = "Outdated" + +-- Show preview +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T934952176"] = "Show preview" + +-- Confirmed +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLRESULTS::T942512558"] = "Confirmed" + -- Test situation UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BEYONDFACTSLAB::BFLSTEPEXTENSIONS::T1049923473"] = "Test situation" @@ -12387,9 +12627,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T613888204"] = "Translation" -- Rewrite and improve a given text for a chosen style. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T722167136"] = "Rewrite and improve a given text for a chosen style." --- Learning -UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T755590027"] = "Learning" - -- Bias of the Day UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T782102948"] = "Bias of the Day" @@ -12405,6 +12642,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T892147062"] = "View and filter AI -- Localization UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T897888480"] = "Localization" +-- Learning & Reflecting +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T912918956"] = "Learning & Reflecting" + -- Turn texts and documents into slides, also as a PowerPoint file in the design of your template. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T943132958"] = "Turn texts and documents into slides, also as a PowerPoint file in the design of your template." @@ -14223,8 +14463,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T3802462536"] = " -- Select the assistant category UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T4193824894"] = "Select the assistant category" --- Learning -UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T755590027"] = "Learning" +-- Learning & Reflecting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTCATEGORYEXTENSIONS::T912918956"] = "Learning & Reflecting" -- This assistant is already running. AI Studio opens the running session instead. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ASSISTANTLAUNCH::ASSISTANTLAUNCHSERVICE::T2575715765"] = "This assistant is already running. AI Studio opens the running session instead." diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 4aa09c3fd..96bf3ce32 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -233,6 +233,7 @@ internal sealed class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/app/MindWork AI Studio/Tools/AssistantCategoryExtensions.cs b/app/MindWork AI Studio/Tools/AssistantCategoryExtensions.cs index 2464fa9af..1daccd522 100644 --- a/app/MindWork AI Studio/Tools/AssistantCategoryExtensions.cs +++ b/app/MindWork AI Studio/Tools/AssistantCategoryExtensions.cs @@ -16,7 +16,7 @@ public static class AssistantCategoryExtensions AssistantCategory.BUSINESS => TB("Business"), AssistantCategory.PRODUCTIVITY => TB("Productivity"), AssistantCategory.DEVELOPMENT => TB("Software Development"), - AssistantCategory.LEARNING => TB("Learning"), + AssistantCategory.LEARNING => TB("Learning & Reflecting"), AssistantCategory.AI_STUDIO => TB("AI Studio Development"), AssistantCategory.OTHER => TB("Other"), diff --git a/app/Tests/BeyondFactsLab/BeyondFactsLabStoreResultTests.cs b/app/Tests/BeyondFactsLab/BeyondFactsLabStoreResultTests.cs new file mode 100644 index 000000000..467bc69be --- /dev/null +++ b/app/Tests/BeyondFactsLab/BeyondFactsLabStoreResultTests.cs @@ -0,0 +1,134 @@ +using AIStudio.Assistants.BeyondFactsLab; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace AIStudio.Tests.BeyondFactsLab; + +/// +/// Checks how the BeyondFacts Lab stores the results of a preparation. +/// +[TestFixture] +public sealed class BeyondFactsLabStoreResultTests +{ + private string dataDirectory = string.Empty; + + [SetUp] + public void CreateDataDirectory() + { + this.dataDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-beyondfacts-lab-{Guid.NewGuid():N}"); + Directory.CreateDirectory(this.dataDirectory); + } + + [TearDown] + public void DeleteDataDirectory() + { + if (Directory.Exists(this.dataDirectory)) + Directory.Delete(this.dataDirectory, true); + } + + [Test] + public async Task APreparationWithoutAResultGetsAnEmptyOne() + { + var store = this.CreateStore(); + var created = await store.CreateAsync("Panel"); + + var result = await store.LoadResultAsync(created.ProjectId, BflResultKind.POSITION_PAPER); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Draft, Is.Null); + Assert.That(result.Confirmed, Is.Null); + } + } + + [Test] + public async Task AResultIsStoredWithItsDraftAndItsConfirmedVersion() + { + var store = this.CreateStore(); + var created = await store.CreateAsync("Panel"); + var confirmedAtUtc = new DateTimeOffset(2026, 11, 10, 9, 0, 0, TimeSpan.Zero); + var result = new BflResult { Draft = new BflResultVersion { Text = "# Positionsblatt", WorkStateRevisions = { [BflStep.GOALS_AND_ROLE] = 3 } } }; + result.Confirm("# Positionsblatt\n\nBestätigt", confirmedAtUtc); + result.Edit(); + result.Draft!.Text = "# Positionsblatt\n\nÜberarbeitet"; + + var changed = await store.SaveResultAsync(created.ProjectId, BflResultKind.POSITION_PAPER, result); + var loaded = await this.CreateStore().LoadResultAsync(created.ProjectId, BflResultKind.POSITION_PAPER); + var others = await this.CreateStore().LoadResultAsync(created.ProjectId, BflResultKind.CONVERSATION_GUIDE); + + using (Assert.EnterMultipleScope()) + { + Assert.That(changed.ModifiedAtUtc, Is.GreaterThanOrEqualTo(created.ModifiedAtUtc)); + Assert.That(loaded.Draft!.Text, Is.EqualTo("# Positionsblatt\n\nÜberarbeitet")); + Assert.That(loaded.Confirmed!.Text, Is.EqualTo("# Positionsblatt\n\nBestätigt")); + Assert.That(loaded.Confirmed.ChangedAtUtc, Is.EqualTo(confirmedAtUtc)); + Assert.That(loaded.Confirmed.WorkStateRevisions, Is.EqualTo(new Dictionary { [BflStep.GOALS_AND_ROLE] = 3 })); + Assert.That(others.Confirmed, Is.Null, "Each kind of result has a file of its own."); + } + } + + [Test] + public async Task ADamagedResultIsNeitherOpenedNorOverwritten() + { + const string DAMAGED = "{ \"draft\": "; + var store = this.CreateStore(); + var created = await store.CreateAsync("Panel"); + var path = await this.WriteResultFileAsync(created.ProjectId, "open-points.json", DAMAGED); + + var results = await store.LoadResultsAsync(created.ProjectId); + + using (Assert.EnterMultipleScope()) + { + Assert.That(results.ContainsKey(BflResultKind.OPEN_POINTS), Is.False); + Assert.That(results.ContainsKey(BflResultKind.POSITION_PAPER), Is.True); + await Assert.ThrowsAsync(async () => await store.LoadResultAsync(created.ProjectId, BflResultKind.OPEN_POINTS)); + await Assert.ThrowsAsync(async () => await store.SaveResultAsync(created.ProjectId, BflResultKind.OPEN_POINTS, new BflResult())); + Assert.That(await File.ReadAllTextAsync(path), Is.EqualTo(DAMAGED)); + } + } + + [Test] + public async Task AResultWithoutItsTextIsNotOpened() + { + const string WITHOUT_TEXT = """{ "schemaVersion": 1, "confirmed": { "text": null, "workStateRevisions": {} } }"""; + var store = this.CreateStore(); + var created = await store.CreateAsync("Panel"); + await this.WriteResultFileAsync(created.ProjectId, "position-paper.json", WITHOUT_TEXT); + + await Assert.ThrowsAsync(async () => await store.LoadResultAsync(created.ProjectId, BflResultKind.POSITION_PAPER)); + } + + [Test] + public async Task AResultOfANewerVersionIsNotOverwritten() + { + const string NEWER = """{ "schemaVersion": 2 }"""; + var store = this.CreateStore(); + var created = await store.CreateAsync("Panel"); + var path = await this.WriteResultFileAsync(created.ProjectId, "reflection-folder.json", NEWER); + + using (Assert.EnterMultipleScope()) + { + await Assert.ThrowsAsync(async () => await store.SaveResultAsync(created.ProjectId, BflResultKind.REFLECTION_FOLDER, new BflResult())); + Assert.That(await File.ReadAllTextAsync(path), Is.EqualTo(NEWER)); + } + } + + [Test] + public async Task AResultOfAMissingPreparationIsNotWritten() + { + var store = this.CreateStore(); + + await Assert.ThrowsAsync(async () => await store.SaveResultAsync(Guid.NewGuid(), BflResultKind.POSITION_PAPER, new BflResult())); + } + + private async Task WriteResultFileAsync(Guid projectId, string fileName, string content) + { + var directory = Path.Combine(this.dataDirectory, BeyondFactsLabStore.ROOT_DIRECTORY_NAME, projectId.ToString("D"), "results"); + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, fileName); + await File.WriteAllTextAsync(path, content); + return path; + } + + private BeyondFactsLabStore CreateStore() => new(NullLogger.Instance, new BflStorageOptions { DataDirectory = this.dataDirectory }); +} \ No newline at end of file diff --git a/app/Tests/BeyondFactsLab/BflPromptLibraryTests.cs b/app/Tests/BeyondFactsLab/BflPromptLibraryTests.cs index 74e19d83b..c59efc323 100644 --- a/app/Tests/BeyondFactsLab/BflPromptLibraryTests.cs +++ b/app/Tests/BeyondFactsLab/BflPromptLibraryTests.cs @@ -68,12 +68,14 @@ public sealed class BflPromptLibraryTests var core = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.CORE_PATH).Select(name => $"{{{{{name}}}}}")); var onboarding = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.ONBOARDING_PATH).Select(name => $"{{{{{name}}}}}")); var workState = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.WORK_STATE_PATH).Select(name => $"{{{{{name}}}}}")); + var result = string.Join("\n", BflPromptLibrary.PlaceholdersOf(BflPromptLibrary.ResultPath(BflResultKind.POSITION_PAPER)).Select(name => $"{{{{{name}}}}}")); using (Assert.EnterMultipleScope()) { Assert.That(BflPromptComposer.Compose(new(core, "Leitfaden", "Schritt", "Anrede"), BflStep.FRAMEWORK, new Dictionary(), [], "Deutsch"), Does.Not.Contain("{{")); Assert.That(BflPromptComposer.ComposeOnboarding(new(onboarding, "Anrede"), "Deutsch"), Does.Not.Contain("{{")); Assert.That(BflPromptComposer.ComposeWorkStateRequest(workState, BflStep.FRAMEWORK, new BflStepFormData()), Does.Not.Contain("{{")); + Assert.That(BflPromptComposer.ComposeResultRequest(result, new Dictionary()), Does.Not.Contain("{{")); } } diff --git a/app/Tests/BeyondFactsLab/BflReflectionFolderTests.cs b/app/Tests/BeyondFactsLab/BflReflectionFolderTests.cs new file mode 100644 index 000000000..fe8aab608 --- /dev/null +++ b/app/Tests/BeyondFactsLab/BflReflectionFolderTests.cs @@ -0,0 +1,200 @@ +using System.Globalization; + +using AIStudio.Assistants.BeyondFactsLab; +using AIStudio.Chat; + +namespace AIStudio.Tests.BeyondFactsLab; + +/// +/// Checks how the BeyondFacts Lab puts the reflection folder together, without a model. +/// +/// +/// The folder repeats what the person confirmed word for word, and it is meant to be passed on: personal disclosures +/// stay out unless the person includes them. +/// +[TestFixture] +public sealed class BflReflectionFolderTests +{ + private static readonly DateTimeOffset NOW = new(2026, 11, 10, 9, 0, 0, TimeSpan.Zero); + + [Test] + public void AConfirmedWorkStateAppearsWordForWordUnderItsStep() + { + var workStates = new Dictionary(); + Confirm(workStates, BflStep.KNOWLEDGE, new() { ["ESTABLISHED"] = "Gesichert-4711\n\n- mit Liste" }); + var established = BflWorkStateSections.ForStep(BflStep.KNOWLEDGE).Single(section => section.Key == "ESTABLISHED"); + + var folder = Build(workStates); + + using (Assert.EnterMultipleScope()) + { + Assert.That(folder, Does.Contain($"## {BflStep.KNOWLEDGE.Title}\n")); + Assert.That(folder, Does.Contain($"### {established.Title}\n\nGesichert-4711\n\n- mit Liste\n")); + Assert.That(folder.IndexOf("Gesichert-4711", StringComparison.Ordinal), Is.GreaterThan(folder.IndexOf($"## {BflStep.KNOWLEDGE.Title}", StringComparison.Ordinal))); + } + } + + [Test] + public void ADraftNeverAppears() + { + var workStates = new Dictionary + { + [BflStep.KNOWLEDGE] = new() { Draft = new(StringComparer.Ordinal) { ["ESTABLISHED"] = "ENTWURF-4711" } }, + }; + + Assert.That(Build(workStates), Does.Not.Contain("ENTWURF-4711")); + } + + [Test] + public void EveryStepAppearsInItsOrderAlsoWithoutAConfirmation() + { + var folder = Build(new Dictionary()); + var positions = BflStep.All.Select(step => folder.IndexOf($"## {step.Title}\n", StringComparison.Ordinal)).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(positions, Has.All.GreaterThanOrEqualTo(0)); + Assert.That(positions, Is.Ordered); + } + } + + [Test] + public void APersonalSectionStaysOutUnlessThePersonIncludesIt() + { + var workStates = new Dictionary(); + Confirm(workStates, BflStep.VALUES_AND_PERSPECTIVES, new() { ["VALUES"] = "Gerechtigkeit", ["PERSONAL_INVOLVEMENT"] = "PERSÖNLICH-4711" }); + + var without = Build(workStates); + var with = Build(workStates, includePersonalDisclosures: true); + + using (Assert.EnterMultipleScope()) + { + Assert.That(without, Does.Not.Contain("PERSÖNLICH-4711")); + Assert.That(without, Does.Contain("Gerechtigkeit")); + Assert.That(with, Does.Contain("PERSÖNLICH-4711")); + } + } + + [Test] + public void TheOpenPointsOfAllStepsAreGatheredAfterTheSteps() + { + var workStates = new Dictionary(); + Confirm(workStates, BflStep.FRAMEWORK, new() { ["FORMAT"] = "Podium", ["OPEN_POINTS"] = "OFFEN-1" }); + Confirm(workStates, BflStep.STRATEGY, new() { ["OPEN_POINTS"] = "OFFEN-5" }); + + var folder = Build(workStates); + var lastStep = folder.IndexOf($"## {BflStep.RESULTS.Title}\n", StringComparison.Ordinal); + + using (Assert.EnterMultipleScope()) + { + Assert.That(folder.IndexOf("OFFEN-1", StringComparison.Ordinal), Is.GreaterThan(lastStep)); + Assert.That(folder.IndexOf("OFFEN-5", StringComparison.Ordinal), Is.GreaterThan(folder.IndexOf("OFFEN-1", StringComparison.Ordinal))); + Assert.That(folder.IndexOf("OFFEN-1", StringComparison.Ordinal), Is.EqualTo(folder.LastIndexOf("OFFEN-1", StringComparison.Ordinal)), "The open points appear once."); + } + } + + [Test] + public void TheMaterialIsListedByName() + { + var materials = new List + { + new() { Name = "Studie_2026.pdf", Kind = BflMaterialKind.DOCUMENT, AddedAtUtc = NOW }, + new() { Name = "Interview.m4a", Kind = BflMaterialKind.RECORDING, AddedAtUtc = NOW.AddMinutes(1) }, + }; + + var folder = BflReflectionFolder.Build(new BflReflectionFolderInput("Panel", new Dictionary(), Forms(), materials, new Dictionary>(), false, CultureInfo.InvariantCulture)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(folder, Does.Contain("- Studie\\_2026\\.pdf (")); + Assert.That(folder, Does.Contain("- Interview\\.m4a (")); + } + } + + [Test] + public void TheConversationsAppearOnlyWhenThePersonAddsThem() + { + var conversations = new Dictionary> + { + [BflStep.STRATEGY] = [new(true, "FRAGE-4711"), new(false, "ANTWORT-4711")], + }; + + var without = Build(new Dictionary()); + var with = Build(new Dictionary(), conversations: conversations); + + using (Assert.EnterMultipleScope()) + { + Assert.That(without, Does.Not.Contain("FRAGE-4711")); + Assert.That(with, Does.Contain("FRAGE-4711")); + Assert.That(with.IndexOf("ANTWORT-4711", StringComparison.Ordinal), Is.GreaterThan(with.IndexOf("FRAGE-4711", StringComparison.Ordinal))); + } + } + + [Test] + public void TheConversationOfAStepWithPersonalDisclosuresStaysOutUnlessThePersonIncludesThem() + { + var conversations = new Dictionary> + { + [BflStep.VALUES_AND_PERSPECTIVES] = [new(true, "BETROFFEN-4711")], + [BflStep.STRATEGY] = [new(true, "STRATEGIE-4711")], + }; + + var without = Build(new Dictionary(), conversations: conversations); + var with = Build(new Dictionary(), includePersonalDisclosures: true, conversations: conversations); + + using (Assert.EnterMultipleScope()) + { + Assert.That(BflReflectionFolder.MayHoldPersonalDisclosures(BflStep.VALUES_AND_PERSPECTIVES), Is.True); + Assert.That(BflReflectionFolder.MayHoldPersonalDisclosures(BflStep.STRATEGY), Is.False); + Assert.That(without, Does.Not.Contain("BETROFFEN-4711")); + Assert.That(without, Does.Contain("STRATEGIE-4711")); + Assert.That(with, Does.Contain("BETROFFEN-4711")); + } + } + + [Test] + public void TheMessagesOfAConversationLeaveOutWhatThePersonNeverSaw() + { + var thread = BflThreadFactory.Create(new BflStepThreadLocation(Guid.NewGuid(), BflStep.STRATEGY)); + thread.Blocks.Add(Block(ChatRole.USER, "VERSTECKT-4711", hidden: true)); + thread.Blocks.Add(Block(ChatRole.AI, "Willkommen")); + thread.Blocks.Add(Block(ChatRole.USER, " Meine Frage ")); + thread.Blocks.Add(Block(ChatRole.AI, " ")); + + var messages = BflReflectionFolder.MessagesOf(thread); + + Assert.That(messages, Is.EqualTo(new[] { new BflConversationMessage(false, "Willkommen"), new BflConversationMessage(true, "Meine Frage") })); + } + + [Test] + public void TheNameOfThePreparationCannotBreakTheHeading() + { + var folder = BflReflectionFolder.Build(new BflReflectionFolderInput("Panel\n# Kopf", new Dictionary(), Forms(), [], new Dictionary>(), false, CultureInfo.InvariantCulture)); + + Assert.That(folder.Split('\n')[0], Does.StartWith("# ").And.Contain("Panel").And.Contain("Kopf")); + } + + private static string Build(IReadOnlyDictionary workStates, bool includePersonalDisclosures = false, IReadOnlyDictionary>? conversations = null) => BflReflectionFolder.Build(new BflReflectionFolderInput("Panel", workStates, Forms(), [], conversations ?? new Dictionary>(), includePersonalDisclosures, CultureInfo.InvariantCulture)); + + private static Dictionary Forms() => BflStep.All.ToDictionary(step => step, _ => new BflStepFormData()); + + private static ContentBlock Block(ChatRole role, string text, bool hidden = false) => new() + { + Time = NOW, + ContentType = ContentType.TEXT, + Role = role, + Content = new ContentText { Text = text }, + HideFromUser = hidden, + }; + + private static void Confirm(Dictionary workStates, BflStep step, Dictionary sections) + { + if (!workStates.TryGetValue(step, out var workState)) + { + workState = new BflWorkState(); + workStates[step] = workState; + } + + workState.Confirm(sections, BflStaleness.UpstreamRevisionsOf(step, workStates), BflStaleness.FingerprintOf(new BflStepFormData()), NOW); + } +} \ No newline at end of file diff --git a/app/Tests/BeyondFactsLab/BflResultRequestTests.cs b/app/Tests/BeyondFactsLab/BflResultRequestTests.cs new file mode 100644 index 000000000..d39c78afb --- /dev/null +++ b/app/Tests/BeyondFactsLab/BflResultRequestTests.cs @@ -0,0 +1,81 @@ +using AIStudio.Assistants.BeyondFactsLab; + +namespace AIStudio.Tests.BeyondFactsLab; + +/// +/// Checks the requests which let the model write the results of the BeyondFacts Lab, and how the lab reads what the +/// model wrote. +/// +[TestFixture] +public sealed class BflResultRequestTests +{ + private static readonly DateTimeOffset NOW = new(2026, 11, 10, 9, 0, 0, TimeSpan.Zero); + + private static IEnumerable WrittenByModel => BflResultKind.All.Where(kind => kind.IsWrittenByModel); + + [Test] + public async Task TheRequestOfEveryResultNamesTheStepsWithoutAConfirmedWorkState() + { + var workStates = new Dictionary(); + workStates[BflStep.FRAMEWORK] = new BflWorkState(); + workStates[BflStep.FRAMEWORK].Confirm(new Dictionary { ["FORMAT"] = "Podium" }, new Dictionary(), string.Empty, NOW); + + foreach (var kind in WrittenByModel) + { + var request = BflPromptComposer.ComposeResultRequest(await BflPromptLibrary.ReadAsync(BflPromptLibrary.ResultPath(kind)), workStates); + + using (Assert.EnterMultipleScope()) + { + Assert.That(request, Does.Not.Contain("{{"), kind.ToString()); + Assert.That(request, Does.Not.Contain($"„{BflStep.FRAMEWORK.Title}“"), kind.ToString()); + foreach (var step in BflStep.All.Where(step => step is not BflStep.FRAMEWORK)) + Assert.That(request, Does.Contain($"„{step.Title}“"), $"{kind}: {step}"); + } + } + } + + [Test] + public void ARequestSaysWhenEveryStepIsConfirmed() + { + var workStates = new Dictionary(); + foreach (var step in BflStep.All) + { + workStates[step] = new BflWorkState(); + workStates[step].Confirm(new Dictionary { ["OPEN_POINTS"] = "Punkt" }, new Dictionary(), string.Empty, NOW); + } + + Assert.That(BflPromptComposer.ComposeResultRequest("{{MISSING_WORK_STATES}}", workStates), Is.EqualTo("keine; alle Schritte sind bestätigt.")); + } + + [Test] + public void ADraftOfAWorkStateCountsAsMissing() + { + var workStates = new Dictionary { [BflStep.STRATEGY] = new() { Draft = new(StringComparer.Ordinal) { ["OPEN_POINTS"] = "Entwurf" } } }; + + Assert.That(BflPromptComposer.ComposeResultRequest("{{MISSING_WORK_STATES}}", workStates), Does.Contain($"„{BflStep.STRATEGY.Title}“")); + } + + [Test] + public void EveryResultOfAModelHasARequestTheTestFolderMayReplace() + { + using (Assert.EnterMultipleScope()) + { + foreach (var kind in WrittenByModel) + Assert.That(BflPromptLibrary.AllPaths, Does.Contain(BflPromptLibrary.ResultPath(kind)), kind.ToString()); + + Assert.That(WrittenByModel.Select(BflPromptLibrary.ResultPath), Is.Unique); + Assert.Throws(() => BflPromptLibrary.ResultPath(BflResultKind.REFLECTION_FOLDER)); + } + } + + [TestCase("# Positionsblatt\n\nText", "# Positionsblatt\n\nText", TestName = "AResultInPlainMarkdownStaysAsItIs")] + [TestCase("```markdown\n# Positionsblatt\n\nText\n```", "# Positionsblatt\n\nText", TestName = "ACodeBlockAroundTheWholeResultIsTakenOff")] + [TestCase("~~~\n# Leitfaden\n~~~", "# Leitfaden", TestName = "ACodeBlockWithTildesAroundTheWholeResultIsTakenOff")] + [TestCase("```\n# Fragen\n```\n\nText\n\n```\nCode\n```", "```\n# Fragen\n```\n\nText\n\n```\nCode\n```", TestName = "SeveralCodeBlocksStayAsTheyAre")] + [TestCase(" # Liste\r\n\r\n- [ ] Punkt \r\n", "# Liste\n\n- [ ] Punkt", TestName = "LineEndingsAndSpacesAroundTheResultAreTidied")] + [TestCase(" \n ", "", TestName = "AnAnswerWithoutTextGivesNoResult")] + public void TheLabReadsTheResultFromTheAnswer(string answer, string expected) + { + Assert.That(BflResultText.Read(answer), Is.EqualTo(expected)); + } +} \ No newline at end of file diff --git a/app/Tests/BeyondFactsLab/BflResultTests.cs b/app/Tests/BeyondFactsLab/BflResultTests.cs new file mode 100644 index 000000000..00ee7edc0 --- /dev/null +++ b/app/Tests/BeyondFactsLab/BflResultTests.cs @@ -0,0 +1,175 @@ +using AIStudio.Assistants.BeyondFactsLab; + +namespace AIStudio.Tests.BeyondFactsLab; + +/// +/// Checks the results of the BeyondFacts Lab: how a draft becomes the confirmed version, and when a result is +/// outdated. +/// +/// +/// A result is outdated as soon as the work state of any step is confirmed for the first time, confirmed anew, or +/// withdrawn after the lab wrote it. +/// +[TestFixture] +public sealed class BflResultTests +{ + private static readonly DateTimeOffset NOW = new(2026, 11, 10, 9, 0, 0, TimeSpan.Zero); + + private static readonly Dictionary SECTIONS = new(StringComparer.Ordinal) { ["GOAL"] = "Informieren" }; + + [Test] + public void AConfirmationKeepsWhatTheDraftWasBuiltFrom() + { + var result = new BflResult { Draft = new BflResultVersion { Text = "Entwurf", WorkStateRevisions = { [BflStep.FRAMEWORK] = 2 } } }; + + result.Confirm("Bestätigt", NOW); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Draft, Is.Null); + Assert.That(result.Confirmed!.Text, Is.EqualTo("Bestätigt")); + Assert.That(result.Confirmed.WorkStateRevisions, Is.EqualTo(new Dictionary { [BflStep.FRAMEWORK] = 2 })); + Assert.That(result.Confirmed.ChangedAtUtc, Is.EqualTo(NOW)); + } + } + + [Test] + public void AResultWithoutADraftCannotBeConfirmed() + { + Assert.Throws(() => new BflResult().Confirm("Text", NOW)); + } + + [Test] + public void EditingAConfirmedResultStartsADraftAndKeepsTheConfirmedVersion() + { + var result = new BflResult { Confirmed = new BflResultVersion { Text = "Bestätigt", WorkStateRevisions = { [BflStep.FRAMEWORK] = 1 } } }; + + result.Edit(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.Draft!.Text, Is.EqualTo("Bestätigt")); + Assert.That(result.Draft.WorkStateRevisions, Is.EqualTo(result.Confirmed.WorkStateRevisions)); + Assert.That(result.Draft.WorkStateRevisions, Is.Not.SameAs(result.Confirmed.WorkStateRevisions)); + Assert.That(result.Confirmed.Text, Is.EqualTo("Bestätigt")); + } + } + + [Test] + public void EditingKeepsADraftWhichIsThereAlready() + { + var result = new BflResult + { + Draft = new BflResultVersion { Text = "Entwurf" }, + Confirmed = new BflResultVersion { Text = "Bestätigt" }, + }; + + result.Edit(); + + Assert.That(result.Draft!.Text, Is.EqualTo("Entwurf")); + } + + [Test] + public void AResultFitsWhileTheWorkStatesStayAsTheyAre() + { + var workStates = new Dictionary(); + Confirm(workStates, BflStep.FRAMEWORK); + Confirm(workStates, BflStep.TEST_SITUATION); + + var version = new BflResultVersion { WorkStateRevisions = new(BflStaleness.ConfirmedRevisionsOf(workStates)) }; + + Assert.That(BflStaleness.IsOutdated(version, workStates), Is.False); + } + + [TestCase(Change.CONFIRMED_FOR_THE_FIRST_TIME, TestName = "AStepConfirmedForTheFirstTimeOutdatesAResult")] + [TestCase(Change.CONFIRMED_ANEW, TestName = "AStepConfirmedAnewOutdatesAResult")] + [TestCase(Change.WITHDRAWN, TestName = "AStepWithdrawnOutdatesAResult")] + [TestCase(Change.CONFIRMED_ANEW_IN_THE_LAST_STEP, TestName = "TheLastStepConfirmedAnewOutdatesAResult")] + public void AChangedWorkStateOutdatesAResult(Change change) + { + var workStates = new Dictionary(); + Confirm(workStates, BflStep.FRAMEWORK); + Confirm(workStates, BflStep.RESULTS); + var version = new BflResultVersion { WorkStateRevisions = new(BflStaleness.ConfirmedRevisionsOf(workStates)) }; + + switch (change) + { + case Change.CONFIRMED_FOR_THE_FIRST_TIME: + Confirm(workStates, BflStep.STRATEGY); + break; + + case Change.CONFIRMED_ANEW: + Confirm(workStates, BflStep.FRAMEWORK); + break; + + case Change.WITHDRAWN: + workStates[BflStep.FRAMEWORK].Withdraw(); + break; + + case Change.CONFIRMED_ANEW_IN_THE_LAST_STEP: + Confirm(workStates, BflStep.RESULTS); + break; + } + + Assert.That(BflStaleness.IsOutdated(version, workStates), Is.True); + } + + [Test] + public void ADraftOfAWorkStateDoesNotOutdateAResult() + { + var workStates = new Dictionary(); + Confirm(workStates, BflStep.FRAMEWORK); + var version = new BflResultVersion { WorkStateRevisions = new(BflStaleness.ConfirmedRevisionsOf(workStates)) }; + + workStates[BflStep.FRAMEWORK].Draft = new(SECTIONS); + workStates[BflStep.KNOWLEDGE] = new BflWorkState { Draft = new(SECTIONS) }; + + Assert.That(BflStaleness.IsOutdated(version, workStates), Is.False); + } + + [Test] + public void TheConfirmedRevisionsNameEveryStepWithAConfirmedWorkState() + { + var workStates = new Dictionary(); + Confirm(workStates, BflStep.KNOWLEDGE); + Confirm(workStates, BflStep.RESULTS); + Confirm(workStates, BflStep.RESULTS); + workStates[BflStep.STRATEGY] = new BflWorkState { Draft = new(SECTIONS) }; + + Assert.That(BflStaleness.ConfirmedRevisionsOf(workStates), Is.EquivalentTo(new Dictionary { [BflStep.KNOWLEDGE] = 1, [BflStep.RESULTS] = 2 })); + } + + [Test] + public void EveryResultHasAFileOfItsOwn() + { + Assert.That(BflResultKind.All.Select(kind => kind.FileName), Is.Unique); + } + + [Test] + public void OnlyTheReflectionFolderIsPutTogetherWithoutAModel() + { + Assert.That(BflResultKind.All.Where(kind => !kind.IsWrittenByModel), Is.EqualTo(new[] { BflResultKind.REFLECTION_FOLDER })); + } + + /// + /// What happens to the work states after the lab wrote a result. + /// + public enum Change + { + CONFIRMED_FOR_THE_FIRST_TIME, + CONFIRMED_ANEW, + WITHDRAWN, + CONFIRMED_ANEW_IN_THE_LAST_STEP, + } + + private static void Confirm(Dictionary workStates, BflStep step) + { + if (!workStates.TryGetValue(step, out var workState)) + { + workState = new BflWorkState(); + workStates[step] = workState; + } + + workState.Confirm(SECTIONS, BflStaleness.UpstreamRevisionsOf(step, workStates), BflStaleness.FingerprintOf(new BflStepFormData()), NOW); + } +} \ No newline at end of file diff --git a/app/Tests/Serialization/JsonTestRoots.cs b/app/Tests/Serialization/JsonTestRoots.cs index b62ec271c..6a4af7438 100644 --- a/app/Tests/Serialization/JsonTestRoots.cs +++ b/app/Tests/Serialization/JsonTestRoots.cs @@ -61,11 +61,12 @@ public static partial class JsonTestRoots yield return Case(new(typeof(IReadOnlyList), SqliteProvenanceStoreClientImplementation.JSON_OPTIONS)); yield return Case(new(typeof(List), SqliteProvenanceStoreClientImplementation.JSON_OPTIONS)); - // The preparations of the BeyondFacts Lab, their forms, materials, and work states, and the preparation selected last: + // The preparations of the BeyondFacts Lab, their forms, materials, work states, and results, and the preparation selected last: yield return Case(new(typeof(BflProject), BeyondFactsLabJson.PERSISTENCE)); yield return Case(new(typeof(BflStepFormData), BeyondFactsLabJson.PERSISTENCE)); yield return Case(new(typeof(BflMaterial), BeyondFactsLabJson.PERSISTENCE)); yield return Case(new(typeof(BflWorkState), BeyondFactsLabJson.PERSISTENCE)); + yield return Case(new(typeof(BflResult), BeyondFactsLabJson.PERSISTENCE)); yield return Case(new(typeof(Guid?), BeyondFactsLabJson.PERSISTENCE), "selected preparation"); }