using System.Diagnostics.CodeAnalysis; namespace AIStudio.Chat; /// /// Changes the messages of a chat on behalf of the user, but never while an answer is being written for it. /// /// /// While an answer is being written, the AI job holds the very same thread as the chat: it writes into /// the answer, and it stores the whole conversation every few seconds. A message the user removed in /// the meantime does not come back, but the job goes on writing an answer nobody sees, and the chat /// stays locked until that answer is complete. Removing the answer and sending another message would /// leave two answers being written for one chat. Changing older messages is no better: the job stores /// the conversation on another thread and could read it in the middle of the change.

/// /// That is why every message is locked while an answer is being written, not only the answer itself. /// Stopping the answer unlocks the chat right away. ///
/// Whether an answer is being written for the chat with the given ID right now. public sealed class ChatBlockActions(Func isAnswerBeingWritten) { /// /// Whether the user may change the messages of the given chat right now. /// /// The chat, or null when there is none yet. /// True when there is a chat and no answer is being written for it. public bool AreAllowed([NotNullWhen(true)] ChatThread? chat) => chat is not null && !isAnswerBeingWritten(chat.ChatId); /// /// Removes a message, together with the hidden prompt an answer was written for. /// /// The chat. /// The content of the message to remove. /// True when the message was removed. public bool TryRemove([NotNullWhen(true)] ChatThread? chat, IContent content) { if (!this.AreAllowed(chat) || !Holds(chat, content)) return false; chat.Remove(content); return true; } /// /// Removes an answer so that it can be written anew for the same question. /// /// The chat. /// The content of the answer to remove. /// True when the answer was removed. public bool TryRemoveForRegenerate([NotNullWhen(true)] ChatThread? chat, IContent answer) { if (!this.AreAllowed(chat) || !Holds(chat, answer)) return false; chat.Remove(answer, removeForRegenerate: true); return true; } /// /// Rolls the chat back, so that the given answer becomes its last message. /// /// /// What the rollback removes and what it keeps is documented at ChatThread.RollBackTo. /// /// The chat. /// The content of the answer to keep as the last message. /// True when later messages were removed. public bool TryRollBack([NotNullWhen(true)] ChatThread? chat, IContent answer) => this.AreAllowed(chat) && chat.RollBackTo(answer); /// /// Makes the last question the draft again, and removes the answer to it. /// /// The chat. /// The content of the question to edit. /// True when the question became the draft. public bool TryEditLastUserBlock([NotNullWhen(true)] ChatThread? chat, IContent question) { if (!this.AreAllowed(chat) || question is not ContentText || !Holds(chat, question)) return false; var lastContent = chat.Blocks.Last().Content; chat.RemoveForEditing(question); if (lastContent is not null) chat.Remove(lastContent); return true; } /// /// Makes a question without an answer the draft again. /// /// The chat. /// The content of the question to edit. /// True when the question became the draft. public bool TryEditLastBlock([NotNullWhen(true)] ChatThread? chat, IContent question) { if (!this.AreAllowed(chat) || question is not ContentText || !Holds(chat, question)) return false; chat.RemoveForEditing(question); return true; } private static bool Holds(ChatThread chat, IContent content) => chat.Blocks.Any(block => block.Content == content); }