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