Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/components/configuration/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,30 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi
});
}, []);

/** Drops pending sub-edits (so the diff dialog doesn't show a partial state)
* and writes a single `undefined` at the section path; on save this routes
* through `resetBaseConfigFieldFn` → `$unset overrides.<sectionPath>`. */
const handleResetSection = useCallback((sectionPath: string) => {
if (!sectionPath) return;
startTransition(() => {
setTouchedPaths((prev) => {
const next = new Set(prev);
next.add(sectionPath);
return next;
});
setEditedValues((prev) => {
const next: t.FlatConfigMap = {};
const prefix = `${sectionPath}.`;
for (const [k, v] of Object.entries(prev)) {
if (k === sectionPath || k.startsWith(prefix)) continue;
next[k] = v;
}
next[sectionPath] = undefined;
return next;
});
});
}, []);

const handleConfirmSave = useCallback(async () => {
if (saving) return;
const touched = [...touchedPaths].filter((p) => p in editedValues);
Expand Down Expand Up @@ -811,6 +835,7 @@ export function ConfigPage({ initialTab, highlightField, initialScope }: t.Confi
editedValues={editedValues}
onFieldChange={handleFieldChange}
onResetField={handleResetField}
onResetSection={handleResetSection}
profileMap={profileMap}
previewMode={false}
previewScope={editingScope}
Expand Down
56 changes: 54 additions & 2 deletions src/components/configuration/ConfigSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Icon } from '@clickhouse/click-ui';
import { useState, useCallback, useEffect, useRef } from 'react';
import type { ReactNode } from 'react';
import type { ReactNode, MouseEvent, KeyboardEvent } from 'react';
import type * as t from '@/types';
import { useLocalize } from '@/hooks';
import { cn } from '@/utils';
Expand Down Expand Up @@ -34,6 +34,9 @@ export function ConfigSection({
defaultExpanded,
inline,
showConfiguredOnly,
sectionPath,
onResetSection,
hasOverrides,
}: t.ConfigSectionProps) {
const localize = useLocalize();
const [isExpanded, setIsExpanded] = useState(() => defaultExpanded ?? configuredCount > 0);
Expand Down Expand Up @@ -63,9 +66,21 @@ export function ConfigSection({
};
}, []);

const handleResetClick = useCallback(
(e: MouseEvent | KeyboardEvent) => {
e.stopPropagation();
if (!sectionPath || !onResetSection) return;
const message = localize('com_config_section_reset_confirm', { section: title });
if (typeof window !== 'undefined' && !window.confirm(message)) return;
onResetSection(sectionPath);
},
[sectionPath, onResetSection, localize, title],
);

if (hidden) return null;

const hasConfigured = configuredCount > 0;
const showResetSection = !!(sectionPath && onResetSection && hasOverrides);

if (inline) {
return (
Expand All @@ -76,7 +91,14 @@ export function ConfigSection({
>
<div className="config-row flex w-full items-center gap-6 rounded-md px-2.5 py-3">
<div className="flex w-[20%] max-w-75 min-w-0 shrink-0 flex-col gap-1 pl-2.5">
<span className="text-sm font-semibold text-(--cui-color-text-default)">{title}</span>
<span className="flex items-center gap-2">
<span className="text-sm font-semibold text-(--cui-color-text-default)">
{title}
</span>
{showResetSection && (
<ResetSectionButton onClick={handleResetClick} title={title} />
)}
</span>
{description && (
<span className="text-xs text-(--cui-color-text-muted)">{description}</span>
)}
Expand Down Expand Up @@ -123,6 +145,9 @@ export function ConfigSection({
{configuredCount}/{totalCount}
</span>
)}
{showResetSection && (
<ResetSectionButton onClick={handleResetClick} title={title} />
)}
</span>
{description && (
<span className="text-xs text-(--cui-color-text-muted)">
Expand Down Expand Up @@ -150,3 +175,30 @@ export function ConfigSection({
</section>
);
}

function ResetSectionButton({
onClick,
title,
}: {
onClick: (e: MouseEvent | KeyboardEvent) => void;
title: string;
}) {
const localize = useLocalize();
return (
<button
type="button"
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick(e);
}
}}
aria-label={localize('com_a11y_reset_section', { name: title })}
className="inline-flex items-center gap-0.5 rounded text-[11px] text-(--cui-color-text-muted) transition-colors hover:text-(--cui-color-text-danger) focus-visible:outline focus-visible:outline-2 focus-visible:outline-(--cui-color-outline)"
>
<Icon name="refresh" size="sm" />
<span>{localize('com_config_reset_section')}</span>
</button>
);
}
104 changes: 92 additions & 12 deletions src/components/configuration/ConfigTabContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { MultiAccordion } from '@clickhouse/click-ui';
import { Icon, MultiAccordion } from '@clickhouse/click-ui';
import type * as t from '@/types';
import { SECTION_RENDERERS, SELF_CONTAINED_SECTION_RENDERERS } from './sections';
import { FieldRenderer, SingleFieldRenderer } from './FieldRenderer';
Expand Down Expand Up @@ -38,6 +38,7 @@ export function ConfigTabContent({
editedValues,
onFieldChange,
onResetField,
onResetSection,
profileMap,
previewMode,
previewScope,
Expand Down Expand Up @@ -83,6 +84,21 @@ export function ConfigTabContent({
return result;
}, [filtering, previewChangedPaths, sections]);

const sectionHasOverrides = useMemo(() => {
const result: Record<string, boolean> = {};
const editedPaths = Object.keys(editedValues);
for (const section of sections) {
const sectionPath = section.schemaKey ?? section.id;
const prefix = `${sectionPath}.`;
const overridden =
(dbOverridePaths &&
[...dbOverridePaths].some((p) => p === sectionPath || p.startsWith(prefix))) ||
editedPaths.some((p) => p === sectionPath || p.startsWith(prefix));
result[section.id] = !!overridden;
}
return result;
}, [sections, dbOverridePaths, editedValues]);

const sectionCounts = useMemo(() => {
return sections.map((s) => {
const key = s.schemaKey ?? s.id;
Expand Down Expand Up @@ -275,6 +291,7 @@ export function ConfigTabContent({
if (group.kind === 'inline') {
const { section } = group;
const counts = countsById[section.id];
const sectionPath = section.schemaKey ?? section.id;
return (
<ConfigSection
key={section.id}
Expand All @@ -286,6 +303,11 @@ export function ConfigTabContent({
totalCount={counts?.total ?? 0}
inline
showConfiguredOnly={showConfiguredOnly}
sectionPath={sectionPath}
onResetSection={
permissions?.canEdit && !fieldsDisabled ? onResetSection : undefined
}
hasOverrides={sectionHasOverrides[section.id]}
>
{renderSectionContent(section)}
</ConfigSection>
Expand All @@ -301,17 +323,34 @@ export function ConfigTabContent({
defaultValue={group.sections.map((s) => s.id)}
data-top-level-accordion
>
{group.sections.map((section) => (
<MultiAccordion.Item
key={section.id}
id={`section-${section.id}`}
data-section-id={`section-${section.id}`}
value={section.id}
title={localize(section.titleKey)}
>
{renderSectionContent(section)}
</MultiAccordion.Item>
))}
{group.sections.map((section) => {
const sectionPath = section.schemaKey ?? section.id;
const canResetSection =
!!onResetSection &&
!!permissions?.canEdit &&
!fieldsDisabled &&
!!sectionHasOverrides[section.id];
const titleNode = canResetSection ? (
<AccordionSectionHeader
title={localize(section.titleKey)}
sectionPath={sectionPath}
onResetSection={onResetSection!}
/>
) : (
localize(section.titleKey)
);
return (
<MultiAccordion.Item
key={section.id}
id={`section-${section.id}`}
data-section-id={`section-${section.id}`}
value={section.id}
title={titleNode}
>
{renderSectionContent(section)}
</MultiAccordion.Item>
);
})}
</MultiAccordion>
);
})}
Expand All @@ -333,3 +372,44 @@ function hasChangedDescendant(
}
return false;
}

function AccordionSectionHeader({
title,
sectionPath,
onResetSection,
}: {
title: string;
sectionPath: string;
onResetSection: (path: string) => void;
}) {
const localize = useLocalize();
const triggerReset = () => {
const message = localize('com_config_section_reset_confirm', { section: title });
if (typeof window !== 'undefined' && !window.confirm(message)) return;
onResetSection(sectionPath);
};
return (
<span className="flex w-full items-center justify-between gap-2">
<span>{title}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
triggerReset();
}}
onKeyDown={(e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
e.stopPropagation();
triggerReset();
}}
aria-label={localize('com_a11y_reset_section', { name: title })}
className="inline-flex items-center gap-0.5 rounded text-[11px] text-(--cui-color-text-muted) transition-colors hover:text-(--cui-color-text-danger) focus-visible:outline focus-visible:outline-2 focus-visible:outline-(--cui-color-outline)"
>
<Icon name="refresh" size="sm" />
<span>{localize('com_config_reset_section')}</span>
</button>
</span>
);
}
3 changes: 3 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
"com_config_default": "Default",
"com_config_reset_default": "Reset to default",
"com_config_reset_confirm": "Reset \"{{field}}\" to its default value? This removes the DB override.",
"com_config_reset_section": "Reset section",
"com_config_section_reset_confirm": "Reset all overrides under \"{{section}}\"? Saving will remove every configured value in this section and revert to defaults from librechat.yaml.",
"com_config_section_configured_count": "{{count}} of {{total}} configured",
"com_config_default_hint": "(default: {{value}})",
"com_config_entry_n": "Entry {{n}}",
Expand Down Expand Up @@ -909,6 +911,7 @@
"com_a11y_rename_entry": "Rename {{name}}",
"com_a11y_remove_override": "Remove override for {{name}}",
"com_a11y_reset_to_default": "Reset {{name}} to default",
"com_a11y_reset_section": "Reset section {{name}}",
"com_a11y_add_item": "Add {{name}}",
"com_access_tab_capabilities": "Capabilities",
"com_cap_title": "System capabilities",
Expand Down
10 changes: 10 additions & 0 deletions src/types/config-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface ConfigTabContentProps {
editedValues: FlatConfigMap;
onFieldChange: (path: string, value: ConfigValue) => void;
onResetField?: (path: string) => void;
onResetSection?: (sectionPath: string) => void;
profileMap?: Record<string, string[]>;
previewMode?: boolean;
previewScope?: ConfigScope;
Expand Down Expand Up @@ -131,6 +132,15 @@ export interface ConfigSectionProps {
defaultExpanded?: boolean;
inline?: boolean;
showConfiguredOnly?: boolean;
/**
* Path of this section within the schema (e.g. `modelSpecs`,
* `endpoints.azureOpenAI`). When provided alongside `onResetSection` and at
* least one configured override under it, the header renders a "Reset
* section" affordance that atomically clears all overrides under the path.
*/
sectionPath?: string;
onResetSection?: (sectionPath: string) => void;
hasOverrides?: boolean;
}

export interface ConfirmSaveDialogProps {
Expand Down