236 lines
9.8 KiB
TypeScript
236 lines
9.8 KiB
TypeScript
import type { ConfigResponse } from '../../../../../packages/api-contracts/src/index.js';
|
|
import type { SettingsDraft, SubredditDraft } from './settings-model.js';
|
|
|
|
interface SectionProps {
|
|
draft: SettingsDraft;
|
|
errors: Record<string, string>;
|
|
onChange(next: SettingsDraft): void;
|
|
}
|
|
|
|
interface NumberFieldProps {
|
|
error?: string;
|
|
label: string;
|
|
name: string;
|
|
min?: number;
|
|
value: string;
|
|
onChange(value: string): void;
|
|
}
|
|
|
|
function NumberField({ error, label, name, min, value, onChange }: NumberFieldProps) {
|
|
const errorId = `${name}-error`;
|
|
return (
|
|
<label className="settings-field">
|
|
<span>{label}</span>
|
|
<input
|
|
aria-describedby={error ? errorId : undefined}
|
|
aria-invalid={error ? 'true' : undefined}
|
|
aria-label={label}
|
|
min={min}
|
|
name={name}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
step="1"
|
|
type="number"
|
|
value={value}
|
|
/>
|
|
{error ? <small className="field-error" id={errorId}>{error}</small> : null}
|
|
</label>
|
|
);
|
|
}
|
|
|
|
export function MonitoringSection({ draft, errors, onChange }: SectionProps) {
|
|
return (
|
|
<section aria-labelledby="monitoring-heading" className="panel settings-section">
|
|
<div className="panel-heading"><div><span className="eyebrow">Discovery</span><h2 id="monitoring-heading">Monitoring</h2></div></div>
|
|
<label className="toggle-field">
|
|
<input
|
|
aria-label="Prioritise recent posts"
|
|
checked={draft.prioritizeRecentPosts}
|
|
onChange={(event) => onChange({ ...draft, prioritizeRecentPosts: event.target.checked })}
|
|
type="checkbox"
|
|
/>
|
|
<span><strong>Prioritise recent posts</strong><small>Prefer posts inside the configured recent window.</small></span>
|
|
</label>
|
|
<NumberField
|
|
error={errors.recentWindowMinutes}
|
|
label="Recent window in minutes"
|
|
min={1}
|
|
name="recent-window"
|
|
onChange={(recentWindowMinutes) => onChange({ ...draft, recentWindowMinutes })}
|
|
value={draft.recentWindowMinutes}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function RateLimitsSection({ draft, errors, onChange }: SectionProps) {
|
|
return (
|
|
<section aria-labelledby="rate-limits-heading" className="panel settings-section">
|
|
<div className="panel-heading"><div><span className="eyebrow">Sending safeguards</span><h2 id="rate-limits-heading">Rate limits</h2></div></div>
|
|
<div className="settings-grid">
|
|
<NumberField
|
|
error={errors.minDelayMinutes}
|
|
label="Minimum delay in minutes"
|
|
min={0}
|
|
name="minimum-delay"
|
|
onChange={(minDelayMinutes) => onChange({ ...draft, minDelayMinutes })}
|
|
value={draft.minDelayMinutes}
|
|
/>
|
|
<NumberField
|
|
error={errors.maxMessagesPerHour}
|
|
label="Maximum messages per hour"
|
|
min={1}
|
|
name="hourly-limit"
|
|
onChange={(maxMessagesPerHour) => onChange({ ...draft, maxMessagesPerHour })}
|
|
value={draft.maxMessagesPerHour}
|
|
/>
|
|
<NumberField
|
|
error={errors.jitterSeconds}
|
|
label="Random jitter in seconds"
|
|
min={0}
|
|
name="jitter"
|
|
onChange={(jitterSeconds) => onChange({ ...draft, jitterSeconds })}
|
|
value={draft.jitterSeconds}
|
|
/>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function updateSubreddit(draft: SettingsDraft, index: number, next: SubredditDraft): SettingsDraft {
|
|
return { ...draft, subreddits: draft.subreddits.map((item, itemIndex) => itemIndex === index ? next : item) };
|
|
}
|
|
|
|
export function BehaviourSection({ config, draft, errors, onChange }: SectionProps & { config: ConfigResponse }) {
|
|
return (
|
|
<section aria-labelledby="behaviour-heading" className="panel settings-section">
|
|
<div className="panel-heading"><div><span className="eyebrow">Runtime behaviour</span><h2 id="behaviour-heading">Behaviour</h2></div></div>
|
|
<label className="toggle-field">
|
|
<input
|
|
aria-label="Dry run mode"
|
|
checked={draft.dryRun}
|
|
onChange={(event) => onChange({ ...draft, dryRun: event.target.checked })}
|
|
type="checkbox"
|
|
/>
|
|
<span><strong>Dry run mode</strong><small>Evaluate posts without sending messages.</small></span>
|
|
</label>
|
|
|
|
<div className="subreddit-heading">
|
|
<div><h3>Subreddits</h3><p>Configure monitored communities and their supported message style.</p></div>
|
|
<button
|
|
className="button secondary"
|
|
onClick={() => onChange({
|
|
...draft,
|
|
subreddits: [
|
|
...draft.subreddits,
|
|
{ name: '', enabled: true, type: 'image', weight: '1', tone: 'default', scripted: '' },
|
|
],
|
|
})}
|
|
type="button"
|
|
>
|
|
Add subreddit
|
|
</button>
|
|
</div>
|
|
{errors.subreddits ? <p className="field-error" role="alert">{errors.subreddits}</p> : null}
|
|
<div className="subreddit-list">
|
|
{draft.subreddits.map((subreddit, index) => (
|
|
<fieldset className="subreddit-row" key={index}>
|
|
<legend>Subreddit {index + 1}</legend>
|
|
<label className="settings-field">
|
|
<span>Name</span>
|
|
<input
|
|
aria-describedby={errors[`subreddits.${index}.name`] ? `subreddit-${index}-name-error` : undefined}
|
|
aria-invalid={errors[`subreddits.${index}.name`] ? 'true' : undefined}
|
|
aria-label={`Subreddit ${index + 1} name`}
|
|
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, name: event.target.value }))}
|
|
type="text"
|
|
value={subreddit.name}
|
|
/>
|
|
{errors[`subreddits.${index}.name`] ? <small className="field-error" id={`subreddit-${index}-name-error`}>{errors[`subreddits.${index}.name`]}</small> : null}
|
|
</label>
|
|
<label className="settings-field">
|
|
<span>Type</span>
|
|
<select
|
|
aria-label={`Subreddit ${index + 1} type`}
|
|
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, type: event.target.value as SubredditDraft['type'] }))}
|
|
value={subreddit.type}
|
|
>
|
|
<option value="image">Image</option><option value="chat">Chat</option>
|
|
</select>
|
|
</label>
|
|
<label className="toggle-field">
|
|
<input
|
|
aria-label={`Subreddit ${index + 1} enabled`}
|
|
checked={subreddit.enabled}
|
|
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, enabled: event.target.checked }))}
|
|
type="checkbox"
|
|
/>
|
|
<span><strong>Enabled</strong><small>Disabled subreddits are not polled.</small></span>
|
|
</label>
|
|
<label className="settings-field">
|
|
<span>Weight</span>
|
|
<input
|
|
aria-describedby={errors[`subreddits.${index}.weight`] ? `subreddit-${index}-weight-error` : undefined}
|
|
aria-invalid={errors[`subreddits.${index}.weight`] ? 'true' : undefined}
|
|
aria-label={`Subreddit ${index + 1} weight`}
|
|
min="0.01"
|
|
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, weight: event.target.value }))}
|
|
step="0.01"
|
|
type="number"
|
|
value={subreddit.weight}
|
|
/>
|
|
{errors[`subreddits.${index}.weight`] ? <small className="field-error" id={`subreddit-${index}-weight-error`}>{errors[`subreddits.${index}.weight`]}</small> : null}
|
|
</label>
|
|
<label className="settings-field">
|
|
<span>Tone</span>
|
|
<select
|
|
aria-label={`Subreddit ${index + 1} tone`}
|
|
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, tone: event.target.value as SubredditDraft['tone'] }))}
|
|
value={subreddit.tone}
|
|
>
|
|
{config.tones.map((tone) => <option key={tone} value={tone}>{tone}</option>)}
|
|
</select>
|
|
</label>
|
|
<label className="settings-field scripted-field">
|
|
<span>Scripted message (optional)</span>
|
|
<textarea
|
|
aria-label={`Subreddit ${index + 1} scripted message`}
|
|
onChange={(event) => onChange(updateSubreddit(draft, index, { ...subreddit, scripted: event.target.value }))}
|
|
rows={2}
|
|
value={subreddit.scripted}
|
|
/>
|
|
</label>
|
|
<button
|
|
className="button secondary remove-subreddit"
|
|
disabled={draft.subreddits.length === 1}
|
|
onClick={() => onChange({
|
|
...draft,
|
|
subreddits: draft.subreddits.filter((_, itemIndex) => itemIndex !== index),
|
|
})}
|
|
type="button"
|
|
>
|
|
Remove subreddit {index + 1}
|
|
</button>
|
|
</fieldset>
|
|
))}
|
|
</div>
|
|
{config.advanced ? (
|
|
<div className="advanced-settings">
|
|
<div><h3>Advanced application configuration</h3><p>Models, prompts, filtering, media sync, and strategy settings. Some changes take effect after a restart.</p></div>
|
|
<label className="settings-field">
|
|
<span>Advanced configuration (JSON)</span>
|
|
<textarea
|
|
aria-invalid={errors.advancedConfigJson ? 'true' : undefined}
|
|
aria-label="Advanced configuration JSON"
|
|
onChange={(event) => onChange({ ...draft, advancedConfigJson: event.target.value })}
|
|
rows={24}
|
|
spellCheck={false}
|
|
value={draft.advancedConfigJson}
|
|
/>
|
|
{errors.advancedConfigJson ? <small className="field-error">{errors.advancedConfigJson}</small> : null}
|
|
</label>
|
|
</div>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|