feat: automate bounded image-sharing conversations

This commit is contained in:
jjbrunton committed 2026-08-08 23:10:52 +01:00
1 parent dcf57273e2
commit 3beb8aada2
15 files changed
+477 -19

No files matched your search

@@ -69,6 +69,7 @@ function ConversationComposer({ conversation, conversationId }: { conversation:
const mediaAskMutation = useSuggestMediaAskMutation(conversationId);
const sendMutation = useSendReplyMutation(conversationId);
const canSend = draft.trim().length > 0 && !sendMutation.isPending;
const automationActive = conversation.automation.status === 'active' && conversation.automation.remainingTurns > 0;
const closeConfirmation = useCallback(() => {
setIsConfirming(false);
@@ -103,10 +104,17 @@ function ConversationComposer({ conversation, conversationId }: { conversation:
<button disabled={suggestionMutation.isPending} onClick={suggestReply} type="button">
{suggestionMutation.isPending ? 'Generating…' : 'Suggest reply'}
</button>
<button disabled={mediaAskMutation.isPending} onClick={suggestMediaAsk} type="button">
{mediaAskMutation.isPending ? 'Generating media ask…' : 'Suggest media ask'}
<button disabled={mediaAskMutation.isPending || !automationActive} onClick={suggestMediaAsk} type="button">
{mediaAskMutation.isPending ? 'Generating media ask…' : 'Suggest media ask'}
</button>
</div>
<div
className={`conversation-automation-status is-${conversation.automation.status}`}
role="status"
>
<strong>{conversation.automation.remainingTurns} automated {conversation.automation.remainingTurns === 1 ? 'turn' : 'turns'} remaining</strong>
<span>{automationStatusMessage(conversation)}</span>
</div>
<label className="sr-only" htmlFor="conversation-reply">Reply to {redditUsernameLabel(conversation.username)}</label>
<textarea
id="conversation-reply"
@@ -149,6 +157,21 @@ function ConversationComposer({ conversation, conversationId }: { conversation:
);
}
function automationStatusMessage(conversation: ConversationDetailResponse): string {
switch (conversation.automation.status) {
case 'image-received':
return 'Image received. Automation is complete.';
case 'human-required':
return 'Automated turns are exhausted. A human should take over.';
case 'boundary':
return 'Automation stopped because the user set a boundary.';
case 'user-overridden':
return 'A manual reply was sent. AI turns are disabled for this conversation.';
default:
return 'The next unread reply can be handled automatically.';
}
}
export function ConversationDetail({ conversationId, onClose }: ConversationDetailProps) {
const detailQuery = useConversationDetailQuery(conversationId);
@@ -85,3 +85,41 @@
.start-conversation-form { grid-template-columns: minmax(12rem, .5fr) minmax(18rem, 1fr) auto; align-items: end; }
.conversation-workspace { grid-template-columns: minmax(17rem, .75fr) minmax(28rem, 1.5fr); }
}
.conversation-automation-status {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 0.875rem;
border: 1px solid var(--border-color, #d7dce2);
border-radius: 0.625rem;
background: rgba(54, 116, 181, 0.08);
}
.conversation-automation-status span {
color: var(--muted-color, #606975);
text-align: right;
}
.conversation-automation-status.is-human-required,
.conversation-automation-status.is-boundary,
.conversation-automation-status.is-user-overridden {
border-color: #d8943a;
background: rgba(216, 148, 58, 0.12);
}
.conversation-automation-status.is-image-received {
border-color: #4d9b68;
background: rgba(77, 155, 104, 0.12);
}
@media (max-width: 640px) {
.conversation-automation-status {
align-items: flex-start;
flex-direction: column;
}
.conversation-automation-status span {
text-align: left;
}
}