import { useState } from 'react'; import type { ConfigResponse } from '../../../../../packages/api-contracts/src/index.js'; import { NumberField, RadioCards, SelectField, SwitchField, TextAreaField, TextField, type RadioCardOption, } from '../../components/form-fields.js'; import { Modal } from '../../components/modal.js'; import { RiskDialog } from '../settings/risk-dialog.js'; import { conversationModes, createEmptySubredditDraft, formatShare, MAX_AUTOMATED_TURNS, previewShare, toSubredditDraft, usesAutomatedTurns, validateSubredditDraft, type ConversationMode, type SubredditConfig, type SubredditDraft, } from './subreddit-model.js'; interface SubredditEditorProps { /** The subreddit being edited, or null to create a new one. */ subreddit: SubredditConfig | null; allSubreddits: readonly SubredditConfig[]; tones: ConfigResponse['tones']; isSaving: boolean; saveError: Error | null; onCancel(): void; onSave(config: SubredditConfig): void; } function errorMessage(error: Error | null): string { return error?.message ?? 'Something went wrong.'; } /** * Edits exactly one subreddit. Because the draft is a single row, validation * errors are keyed by plain field name — there are no array indices to shift * when rows are added or removed elsewhere. */ export function SubredditEditor({ allSubreddits, isSaving, onCancel, onSave, saveError, subreddit, tones, }: SubredditEditorProps) { const isNew = subreddit === null; const [draft, setDraft] = useState(() => subreddit ? toSubredditDraft(subreddit) : createEmptySubredditDraft()); const [errors, setErrors] = useState>({}); const [pendingRisks, setPendingRisks] = useState<{ config: SubredditConfig; reasons: string[] } | null>(null); const takenNames = allSubreddits .filter((item) => item.name !== subreddit?.name) .map((item) => item.name); function update(next: Partial) { setErrors({}); setDraft((current) => ({ ...current, ...next })); } function submit() { const validation = validateSubredditDraft(draft, takenNames); setErrors(validation.errors); if (!validation.config) return; const reasons: string[] = []; const previousMode = subreddit?.conversation.mode; const nextMeta = conversationModes.find((mode) => mode.value === validation.config!.conversation.mode); if (nextMeta?.isAutonomous && previousMode !== validation.config.conversation.mode) { reasons.push( `r/${validation.config.name} will reply autonomously, without approval for each message.` + (validation.config.conversation.mode === 'r4r-media' ? ' After rapport it can make one optional, non-explicit image request.' : ' It will not request sexual media.'), ); reasons.push('Conversations already in progress keep the policy they started with.'); } if (reasons.length > 0) { setPendingRisks({ config: validation.config, reasons }); return; } onSave(validation.config); } const share = previewShare(draft, allSubreddits, subreddit?.name ?? null); const turnOptions: RadioCardOption[] = conversationModes.map((mode) => ({ value: mode.value, label: mode.label, description: mode.description, isRisky: mode.isAutonomous, detail: usesAutomatedTurns(mode.value) ? (
update({ maxAutomatedTurns })} suffix="turns" value={draft.maxAutomatedTurns} /> {mode.value === 'r4r-media' ? ( update({ rapportTurnsBeforeMediaAsk })} suffix="turns" value={draft.rapportTurnsBeforeMediaAsk} /> ) : null}
) : undefined, })); return ( <>
{isNew ? 'New subreddit' : 'Edit subreddit'}

{isNew ? 'Add a subreddit' : `r/${subreddit.name}`}

Community
update({ name })} placeholder="casualconversation" prefix="r/" value={draft.name} /> update({ type })} options={[{ value: 'image', label: 'Image posts' }, { value: 'chat', label: 'Chat and text posts' }]} value={draft.type} /> update({ targetingMode })} options={draft.type === 'chat' ? [{ value: 'post-author', label: 'Post author' }] : [ { value: 'post-author', label: 'Post author' }, { value: 'comment-author', label: 'Qualifying commenter' }, ]} value={draft.type === 'chat' ? 'post-author' : draft.targetingMode} />
Targeting update({ enabled })} />
update({ weight })} step="any" value={draft.weight} /> {/* Weight only means something as a proportion, so show the proportion it will actually produce. */}
Share of polling {formatShare(share)} {draft.enabled ? 'Of all enabled subreddits, once saved.' : 'Nothing while polling is off.'}
Voice
update({ tone })} options={tones.map((tone) => ({ value: tone, label: tone }))} value={draft.tone} />
update({ scripted })} rows={3} value={draft.scripted} />
update({ conversationMode })} options={turnOptions} value={draft.conversationMode} />
{Object.keys(errors).length > 0 ? ( {Object.keys(errors).length === 1 ? 'Resolve 1 problem before saving.' : `Resolve ${Object.keys(errors).length} problems before saving.`} ) : null} {saveError ? {errorMessage(saveError)} : null}
{pendingRisks ? ( setPendingRisks(null)} onConfirm={() => onSave(pendingRisks.config)} reasons={pendingRisks.reasons} /> ) : null} ); }