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 (
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.
);
}
/**
* 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 }));
}