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") }; }