Add per-subreddit R4R conversation modes

This commit is contained in:
jjbrunton committed 2026-08-09 11:30:32 +01:00
1 parent 23d4c3a320
commit 0f76bcf59a
14 files changed
+343 -24

No files matched your search

@@ -76,6 +76,13 @@ export function SettingsEditor({ config, section }: SettingsEditorProps) {
if (validation.data.maxMessagesPerHour > 20 && validation.data.maxMessagesPerHour > persistedLimit) {
reasons.push(`The limit will allow ${validation.data.maxMessagesPerHour} messages per hour.`);
}
const enablesAutonomousR4R = validation.data.payload.subreddits?.some((subreddit, index) => (
subreddit.conversation.mode === 'r4r'
&& persistedDraft.subreddits[index]?.conversationMode !== 'r4r'
));
if (enablesAutonomousR4R) {
reasons.push('Autonomous R4R will send conversational replies without approval for the selected subreddit. Existing conversations keep their snapshotted policy.');
}
if (reasons.length > 0) {
setPendingRiskySave({ payload: validation.data.payload, reasons });
@@ -14,6 +14,8 @@ export interface SubredditDraft {
weight: string;
tone: z.infer<typeof ToneKeySchema>;
scripted: string;
conversationMode: 'legacy' | 'r4r' | 'manual';
maxAutomatedTurns: string;
}
export interface SettingsDraft {
@@ -73,6 +75,8 @@ export function createSettingsDraft(config: ConfigResponse): SettingsDraft {
weight: subreddit.weight === undefined ? '' : String(subreddit.weight),
tone: subreddit.tone,
scripted: subreddit.scripted ?? '',
conversationMode: subreddit.conversation.mode,
maxAutomatedTurns: String(subreddit.conversation.maxAutomatedTurns),
})),
advancedConfigJson: config.advanced ? JSON.stringify(config.advanced, null, 2) : '',
};
@@ -89,6 +93,9 @@ export function validateSettingsDraft(draft: SettingsDraft): SettingsValidationR
if (subreddit.weight.trim() !== '' && Number(subreddit.weight) <= 0) {
requiredErrors[`subreddits.${index}.weight`] = 'Subreddit weight must be greater than 0.';
}
if (subreddit.conversationMode === 'r4r' && subreddit.maxAutomatedTurns.trim() === '') {
requiredErrors[`subreddits.${index}.maxAutomatedTurns`] = 'Maximum automated turns is required.';
}
});
if (Object.keys(requiredErrors).length > 0) return { errors: requiredErrors };
@@ -119,6 +126,12 @@ export function validateSettingsDraft(draft: SettingsDraft): SettingsValidationR
weight: subreddit.weight.trim() === '' ? undefined : Number(subreddit.weight),
tone: subreddit.tone,
scripted: subreddit.scripted.trim() === '' ? undefined : subreddit.scripted,
conversation: {
mode: subreddit.conversationMode,
maxAutomatedTurns: subreddit.conversationMode === 'r4r'
? Number(subreddit.maxAutomatedTurns)
: 3,
},
})),
});
@@ -122,7 +122,7 @@ export function BehaviourSection({ config, draft, errors, onChange }: SectionPro
...draft,
subreddits: [
...draft.subreddits,
{ name: '', enabled: true, type: 'image', weight: '1', tone: 'default', scripted: '' },
{ name: '', enabled: true, type: 'image', weight: '1', tone: 'default', scripted: '', conversationMode: 'legacy', maxAutomatedTurns: '3' },
],
})}
type="button"
@@ -190,6 +190,43 @@ export function BehaviourSection({ config, draft, errors, onChange }: SectionPro
{config.tones.map((tone) => <option key={tone} value={tone}>{tone}</option>)}
</select>
</label>
<label className="settings-field">
<span>Conversation behaviour</span>
<select
aria-label={`Subreddit ${index + 1} conversation behaviour`}
onChange={(event) => onChange(updateSubreddit(draft, index, {
...subreddit,
conversationMode: event.target.value as SubredditDraft['conversationMode'],
}))}
value={subreddit.conversationMode}
>
<option value="legacy">Legacy media asks</option>
<option value="r4r">Autonomous R4R conversation</option>
<option value="manual">Manual only</option>
</select>
<small>{subreddit.conversationMode === 'r4r'
? 'Continues general conversation autonomously. It will not request sexual media.'
: subreddit.conversationMode === 'manual'
? 'Incoming replies are marked for human handling and are never answered automatically.'
: 'Preserves the existing three-turn media-request flow.'}</small>
</label>
{subreddit.conversationMode === 'r4r' ? (
<label className="settings-field">
<span>Maximum automated turns</span>
<input
aria-describedby={errors[`subreddits.${index}.maxAutomatedTurns`] ? `subreddit-${index}-max-turns-error` : undefined}
aria-invalid={errors[`subreddits.${index}.maxAutomatedTurns`] ? 'true' : undefined}
aria-label={`Subreddit ${index + 1} maximum automated turns`}
max="20"
min="1"
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, maxAutomatedTurns: event.target.value }))}
step="1"
type="number"
value={subreddit.maxAutomatedTurns}
/>
{errors[`subreddits.${index}.maxAutomatedTurns`] ? <small className="field-error" id={`subreddit-${index}-max-turns-error`}>{errors[`subreddits.${index}.maxAutomatedTurns`]}</small> : null}
</label>
) : null}
<label className="settings-field scripted-field">
<span>Scripted message (optional)</span>
<textarea