import React, { useMemo, useState } from 'react'; import { RotateCcw } from 'lucide-react'; /** * Settings modal for the user-tunable repetition / failsafe limits. * * The schema (defaults, bounds, descriptions, group assignments) is * fetched from GET /api/chat/limits/defaults so the entire UI is * server-driven: adding a knob in `services.models.ConversationLimits` * makes it appear here automatically. The user's overrides are * persisted to localStorage and sent on the next /chat/start, where * the backend re-clamps them to the declared bounds. * * "Effective" values shown to the user are: override (if set) -> * server default. Values are pre-clamped client-side too so the * stepper UI stays in range. */ export default function ConversationLimitsModal({ isOpen, schema, overrides, onClose, onChange, onResetAll, }) { const [draft, setDraft] = useState(() => ({ ...(overrides || {}) })); // Reset draft whenever the modal is reopened so we don't leak edits // from a previous open. React.useEffect(() => { if (isOpen) setDraft({ ...(overrides || {}) }); }, [isOpen, overrides]); const grouped = useMemo(() => groupFields(schema), [schema]); if (!isOpen) return null; if (!schema) { return (

Conversation limits

Loading limits...
); } const handleChange = (field, value) => { const next = { ...draft, [field]: value }; setDraft(next); onChange?.(next); }; const handleResetField = (field) => { const next = { ...draft }; delete next[field]; setDraft(next); onChange?.(next); }; const handleResetAll = () => { setDraft({}); onResetAll?.(); }; return (

Conversation limits

These knobs control how long each phase of the discussion runs and when the conversation pauses for a Continue confirmation. Changes apply to the next chat you start.
{grouped.map(({ group, fields }) => (
{group}
{fields.map((f) => ( handleChange(f.field, v)} onResetField={() => handleResetField(f.field)} /> ))}
))}
); } function LimitRow({ field, label, help, defaultValue, min, max, override, onChange, onResetField, }) { const isOverride = override !== undefined && override !== null; const effective = isOverride ? override : defaultValue; const setClamped = (raw) => { if (raw === '' || raw === null || raw === undefined) { // Empty input = revert to default for this field. onResetField(); return; } let n = parseInt(raw, 10); if (Number.isNaN(n)) return; if (n < min) n = min; if (n > max) n = max; onChange(n); }; return (
setClamped(e.target.value)} className="ccai-limit-input" /> ({min}-{max}, default {defaultValue}) {isOverride && ( )}
{help}
); } /** * Convert the flat `descriptions` map into [{group, fields[]}] in * the order the groups first appear, then the order each field is * declared in `bounds` (which mirrors the dataclass field order). */ function groupFields(schema) { if (!schema) return []; const { defaults, bounds, descriptions } = schema; const orderedFields = Object.keys(bounds); const seen = new Map(); for (const field of orderedFields) { const desc = descriptions[field] || {}; const group = desc.group || 'Other'; if (!seen.has(group)) seen.set(group, []); seen.get(group).push({ field, label: desc.label || field, help: desc.help || '', defaultValue: defaults[field], min: bounds[field].min, max: bounds[field].max, }); } return Array.from(seen.entries()).map(([group, fields]) => ({ group, fields })); }