Skip to content

Commit 455543e

Browse files
authored
feat: persistent user settings v2 (#799)
1 parent 96fd51e commit 455543e

File tree

8 files changed

+147
-86
lines changed

8 files changed

+147
-86
lines changed

package-lock.json

Lines changed: 22 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ragbits-chat/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# CHANGELOG
22

33
## Unreleased
4+
- Improve user settings storage when history is disabled (#799)
45
- Remove redundant test for `/api/config` endpoint (#795)
56
- Fix bug causing infinite initialization screen (#793)
67
- Fix bug that caused messages to be sent when changing chat settings; simplify and harden history logic (#791)

typescript/ui/__tests__/integration/intergation.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ describe("Integration tests", () => {
184184
const submitButton = await screen.findByText("Save");
185185
await user.click(submitButton);
186186

187+
await waitFor(async () => {
188+
expect(await screen.queryByRole("dialog")).not.toBeInTheDocument();
189+
});
190+
187191
const input = await screen.findByRole("textbox");
188192
fireEvent.change(input, { target: { value: "Test message 3" } });
189193

typescript/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"react-markdown": "^9.0.3",
3939
"react-router": "^7.7.1",
4040
"remark-gfm": "^4.0.0",
41+
"usehooks-ts": "^3.1.1",
4142
"uuid": "^11.1.0",
4243
"zod": "^3.24.2",
4344
"zustand": "^5.0.6"

typescript/ui/src/core/forms/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ export { default as SelectWidget } from "./components/SelectWidget";
55
export { default as FieldTemplate } from "./components/FieldTemplate";
66

77
// Shared form utilities
8-
export { useTransformErrors } from "./utils";
8+
export { transformErrors } from "./utils";
Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import { useCallback } from "react";
21
import { RJSFValidationError } from "@rjsf/utils";
32

43
/**
54
* Shared error transformation function for form validation
65
*/
7-
export const useTransformErrors = () => {
8-
return useCallback((errors: RJSFValidationError[]) => {
9-
return errors.map((error) => {
10-
if (error.name === "minLength" || error.name === "required") {
11-
return { ...error, message: "Field must not be empty" };
12-
}
13-
return error;
14-
});
15-
}, []);
6+
export const transformErrors = (errors: RJSFValidationError[]) => {
7+
return errors.map((error) => {
8+
if (error.name === "minLength" || error.name === "required") {
9+
return { ...error, message: "Field must not be empty" };
10+
}
11+
return error;
12+
});
1613
};

typescript/ui/src/plugins/ChatOptionsPlugin/components/ChatOptionsForm.tsx

Lines changed: 110 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,56 @@ import {
99
import { Icon } from "@iconify/react";
1010
import DelayedTooltip from "../../../core/components/DelayedTooltip";
1111
import { useConfigContext } from "../../../core/contexts/ConfigContext/useConfigContext";
12-
import { FormTheme, useTransformErrors } from "../../../core/forms";
12+
import { FormTheme, transformErrors } from "../../../core/forms";
1313
import validator from "@rjsf/validator-ajv8";
1414
import { IChangeEvent } from "@rjsf/core";
15-
import { useEffect } from "react";
15+
import { FormEvent, useEffect, useRef } from "react";
1616
import { getDefaultBasedOnSchemaType } from "@rjsf/utils/lib/schema/getDefaultFormState";
1717
import {
1818
useConversationProperty,
1919
useHistoryActions,
2020
} from "../../../core/stores/HistoryStore/selectors";
2121
import { useHistoryStore } from "../../../core/stores/HistoryStore/useHistoryStore";
22+
import { useLocalStorage } from "usehooks-ts";
23+
import { pluginManager } from "../../../core/utils/plugins/PluginManager";
24+
import { ChatHistoryPlugin } from "../../ChatHistoryPlugin";
25+
import { AnimationDefinition } from "framer-motion";
26+
27+
const CHAT_OPTIONS_KEY = "ragbits-no-history-chat-options";
2228

2329
export default function ChatOptionsForm() {
2430
const { isOpen, onOpen, onClose } = useDisclosure();
2531
const chatOptions = useConversationProperty((s) => s.chatOptions);
32+
// Needed to solve flicker to default settings when the modal is closing
33+
const pendingSettingsRef = useRef<Record<string, unknown> | null>(null);
2634
const { setChatOptions, initializeChatOptions } = useHistoryActions();
2735
const currentConversation = useHistoryStore((s) => s.currentConversation);
2836
const {
2937
config: { user_settings: userSettings },
3038
} = useConfigContext();
39+
const [savedSettings, setSettings] = useLocalStorage<Record<
40+
string,
41+
unknown
42+
> | null>(CHAT_OPTIONS_KEY, null);
3143

3244
const schema = userSettings?.form;
3345

34-
const onOpenChange = () => {
35-
onClose();
46+
const ensureSyncWithStorage = (data: Record<string, unknown>) => {
47+
// Sync to localStorage only when history is disabled
48+
if (pluginManager.isPluginActivated(ChatHistoryPlugin.name)) {
49+
return;
50+
}
51+
52+
setSettings(data);
53+
};
54+
55+
const onModalOpen = () => {
56+
onOpen();
3657
};
3758

38-
const handleFormSubmit = (data: IChangeEvent) => {
39-
setChatOptions(data.formData);
59+
const handleFormSubmit = (data: IChangeEvent, event: FormEvent) => {
60+
event.preventDefault();
61+
pendingSettingsRef.current = data.formData;
4062
onClose();
4163
};
4264

@@ -46,20 +68,49 @@ export default function ChatOptionsForm() {
4668
}
4769

4870
const defaultState = getDefaultBasedOnSchemaType(validator, schema);
49-
setChatOptions(defaultState);
71+
pendingSettingsRef.current = defaultState;
5072
onClose();
5173
};
5274

53-
const transformErrors = useTransformErrors();
75+
const onOpenChange = () => {
76+
onClose();
77+
};
78+
79+
const onAnimationComplete = (definition: AnimationDefinition) => {
80+
if (definition !== "exit" || !pendingSettingsRef.current) {
81+
return;
82+
}
83+
84+
const settings = pendingSettingsRef.current;
85+
setChatOptions(settings);
86+
ensureSyncWithStorage(settings);
87+
pendingSettingsRef.current = null;
88+
};
5489

5590
useEffect(() => {
5691
if (!schema) {
5792
return;
5893
}
5994

6095
const defaultState = getDefaultBasedOnSchemaType(validator, schema);
61-
initializeChatOptions(defaultState);
62-
}, [initializeChatOptions, schema, currentConversation]);
96+
// When history is active, use default state for new conversations
97+
if (pluginManager.isPluginActivated(ChatHistoryPlugin.name)) {
98+
initializeChatOptions(defaultState);
99+
// Otherwise if we have saved settings, use them
100+
} else if (savedSettings !== null) {
101+
initializeChatOptions(savedSettings);
102+
// Otherwise just use defaults
103+
} else {
104+
initializeChatOptions(defaultState);
105+
setSettings(defaultState);
106+
}
107+
}, [
108+
initializeChatOptions,
109+
schema,
110+
currentConversation,
111+
savedSettings,
112+
setSettings,
113+
]);
63114

64115
if (!schema) {
65116
return null;
@@ -73,62 +124,64 @@ export default function ChatOptionsForm() {
73124
variant="ghost"
74125
className="p-0"
75126
aria-label="Open chat options"
76-
onPress={onOpen}
127+
onPress={onModalOpen}
77128
data-testid="open-chat-options"
78129
>
79130
<Icon icon="heroicons:cog-6-tooth" />
80131
</Button>
81132
</DelayedTooltip>
82133

83-
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
134+
<Modal
135+
isOpen={isOpen}
136+
onOpenChange={onOpenChange}
137+
motionProps={{
138+
onAnimationComplete,
139+
}}
140+
>
84141
<ModalContent>
85-
{(onClose) => (
86-
<>
87-
<ModalHeader className="text-default-900 flex flex-col gap-1">
88-
{schema.title || "Chat Options"}
89-
</ModalHeader>
90-
<ModalBody>
91-
<div className="flex flex-col gap-4">
92-
<FormTheme
93-
schema={schema}
94-
validator={validator}
95-
formData={chatOptions}
96-
onSubmit={handleFormSubmit}
97-
transformErrors={transformErrors}
98-
liveValidate
142+
<ModalHeader className="text-default-900 flex flex-col gap-1">
143+
{schema.title || "Chat Options"}
144+
</ModalHeader>
145+
<ModalBody>
146+
<div className="flex flex-col gap-4">
147+
<FormTheme
148+
schema={schema}
149+
validator={validator}
150+
formData={chatOptions}
151+
onSubmit={handleFormSubmit}
152+
transformErrors={transformErrors}
153+
liveValidate
154+
>
155+
<div className="flex justify-end gap-4 py-4">
156+
<Button
157+
className="mr-auto"
158+
color="primary"
159+
variant="light"
160+
onPress={onRestoreDefaults}
161+
aria-label="Restore default user settings"
162+
>
163+
Restore defaults
164+
</Button>
165+
<Button
166+
color="danger"
167+
variant="light"
168+
onPress={onClose}
169+
aria-label="Close chat options form"
170+
>
171+
Cancel
172+
</Button>
173+
<Button
174+
color="primary"
175+
type="submit"
176+
aria-label="Save chat options"
177+
data-testid="chat-options-submit"
99178
>
100-
<div className="flex justify-end gap-4 py-4">
101-
<Button
102-
className="mr-auto"
103-
color="primary"
104-
variant="light"
105-
onPress={onRestoreDefaults}
106-
aria-label="Restore default user settings"
107-
>
108-
Restore defaults
109-
</Button>
110-
<Button
111-
color="danger"
112-
variant="light"
113-
onPress={onClose}
114-
aria-label="Close chat options form"
115-
>
116-
Cancel
117-
</Button>
118-
<Button
119-
color="primary"
120-
type="submit"
121-
aria-label="Save chat options"
122-
data-testid="chat-options-submit"
123-
>
124-
Save
125-
</Button>
126-
</div>
127-
</FormTheme>
179+
Save
180+
</Button>
128181
</div>
129-
</ModalBody>
130-
</>
131-
)}
182+
</FormTheme>
183+
</div>
184+
</ModalBody>
132185
</ModalContent>
133186
</Modal>
134187
</>

typescript/ui/src/plugins/FeedbackPlugin/components/FeedbackForm.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { FeedbackType } from "@ragbits/api-client-react";
1111
import { Icon } from "@iconify/react";
1212
import DelayedTooltip from "../../../core/components/DelayedTooltip";
1313
import { useConfigContext } from "../../../core/contexts/ConfigContext/useConfigContext";
14-
import { FormTheme, useTransformErrors } from "../../../core/forms";
14+
import { FormTheme, transformErrors } from "../../../core/forms";
1515
import validator from "@rjsf/validator-ajv8";
1616
import { IChangeEvent } from "@rjsf/core";
1717
import { ChatMessage } from "../../../types/history";
@@ -82,8 +82,6 @@ export default function FeedbackForm({ message }: FeedbackFormProps) {
8282
onOpen();
8383
};
8484

85-
const transformErrors = useTransformErrors();
86-
8785
if (!schema) {
8886
return null;
8987
}

0 commit comments

Comments
 (0)