Skip to content

Commit d0a4ce3

Browse files
committed
fix: persist custom_prompt on reload and replace autosave with save button
- Include custom_prompt in getProfile() response (was silently dropped) - Replace autosave with explicit Save button for custom_prompt - Track dirty state so button only shows when prompt has unsaved changes
1 parent 59deb3a commit d0a4ce3

4 files changed

Lines changed: 64 additions & 19 deletions

File tree

apps/web/src/assets/styles/features/auth/profile.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,17 @@
246246
opacity: 0.5;
247247
}
248248

249+
.profile-card-prompt-footer {
250+
display: flex;
251+
align-items: center;
252+
justify-content: space-between;
253+
margin-top: var(--spacing-xxsmall);
254+
}
255+
249256
.profile-card-char-count {
250-
text-align: right;
251257
font-size: 0.75rem;
252258
color: var(--font-color);
253259
opacity: 0.5;
254-
margin-top: var(--spacing-xxsmall);
255260
}
256261

257262
/* Extra fields inside profile card */

apps/web/src/features/auth/components/profile/tabs/ProfileInfo/ProfileView.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ interface ProfileViewProps {
5252
setUsername: (value: string) => void;
5353
customPrompt: string;
5454
setCustomPrompt: (value: string) => void;
55+
isPromptDirty: boolean;
56+
isSavingPrompt: boolean;
57+
onSaveCustomPrompt: () => void;
5558
errorProfile: string;
5659
isErrorProfileQuery: boolean;
5760
errorProfileQueryMessage: string | undefined;
@@ -84,6 +87,9 @@ const ProfileView = ({
8487
setUsername,
8588
customPrompt,
8689
setCustomPrompt,
90+
isPromptDirty,
91+
isSavingPrompt,
92+
onSaveCustomPrompt,
8793
errorProfile,
8894
isErrorProfileQuery,
8995
errorProfileQueryMessage,
@@ -227,7 +233,19 @@ const ProfileView = ({
227233
maxLength={2000}
228234
disabled={isLoading}
229235
/>
230-
<div className="profile-card-char-count">{customPrompt.length}/2000</div>
236+
<div className="profile-card-prompt-footer">
237+
<div className="profile-card-char-count">{customPrompt.length}/2000</div>
238+
{isPromptDirty && (
239+
<button
240+
type="button"
241+
className="btn-primary size-s"
242+
onClick={onSaveCustomPrompt}
243+
disabled={isSavingPrompt || isLoading}
244+
>
245+
{isSavingPrompt ? 'Speichert…' : 'Speichern'}
246+
</button>
247+
)}
248+
</div>
231249
</div>
232250

233251
{/* Hidden profile fields - only shown when not from Keycloak */}

apps/web/src/features/auth/components/profile/tabs/ProfileInfo/index.tsx

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ const ProfileInfoTabContainer = ({
8585
const [isDeletingAccount, setIsDeletingAccount] = useState(false);
8686
const [deleteAccountError, setDeleteAccountError] = useState('');
8787
const isInitialized = useRef(false);
88+
const savedCustomPromptRef = useRef('');
89+
const [isPromptDirty, setIsPromptDirty] = useState(false);
90+
const [isSavingPrompt, setIsSavingPrompt] = useState(false);
8891

8992
const updateProfileMutation = useMutation({
9093
mutationFn: async (profileData: ProfileUpdateData) => {
@@ -173,7 +176,6 @@ const ProfileInfoTabContainer = ({
173176
display_name: fullDisplayName,
174177
username: username || null,
175178
email: email?.trim() || null,
176-
custom_prompt: customPrompt || null,
177179
};
178180
try {
179181
const timeoutPromise = new Promise((_, reject) =>
@@ -189,30 +191,44 @@ const ProfileInfoTabContainer = ({
189191
onErrorProfileMessage('Speichern dauert zu lange. Bitte versuchen Sie es erneut.');
190192
}
191193
}
192-
}, [
193-
displayName,
194-
username,
195-
email,
196-
customPrompt,
197-
user?.username,
198-
profile,
199-
updateProfile,
200-
onErrorProfileMessage,
201-
]);
194+
}, [displayName, username, email, user?.username, profile, updateProfile, onErrorProfileMessage]);
195+
196+
const handleCustomPromptChange = useCallback((value: string) => {
197+
setCustomPrompt(value);
198+
setIsPromptDirty(value !== savedCustomPromptRef.current);
199+
}, []);
200+
201+
const handleSaveCustomPrompt = useCallback(async () => {
202+
if (!profile || !isInitialized.current) return;
203+
setIsSavingPrompt(true);
204+
try {
205+
await updateProfile({ custom_prompt: customPrompt || null });
206+
savedCustomPromptRef.current = customPrompt;
207+
setIsPromptDirty(false);
208+
setErrorProfile('');
209+
onSuccessMessage('Anweisungen gespeichert');
210+
} catch (error) {
211+
console.error('Custom prompt save error:', error);
212+
setErrorProfile('Fehler beim Speichern der Anweisungen.');
213+
onErrorProfileMessage('Fehler beim Speichern der Anweisungen.');
214+
} finally {
215+
setIsSavingPrompt(false);
216+
}
217+
}, [customPrompt, profile, updateProfile, onSuccessMessage, onErrorProfileMessage]);
202218

203219
const { resetTracking } = useAutosave({
204220
saveFunction: async () => {
205221
await stateBasedSave();
206222
},
207223
formRef: {
208-
getValues: () => ({ displayName, username, email, customPrompt }),
224+
getValues: () => ({ displayName, username, email }),
209225
watch: (callback: (value: Record<string, unknown>, { name }: { name?: string }) => void) => ({
210226
unsubscribe: () => {},
211227
}),
212228
},
213229
enabled: profile && isInitialized.current,
214230
debounceMs: 2000,
215-
getFieldsToTrack: () => ['displayName', 'username', 'email', 'customPrompt'],
231+
getFieldsToTrack: () => ['displayName', 'username', 'email'],
216232
onError: (error: unknown) => {
217233
console.error('Profile autosave failed:', error);
218234
setErrorProfile('Automatisches Speichern fehlgeschlagen.');
@@ -226,7 +242,9 @@ const ProfileInfoTabContainer = ({
226242
setDisplayName(formFields.displayName);
227243
setUsername(formFields.username);
228244
setEmail(formFields.email);
229-
setCustomPrompt((profile as { custom_prompt?: string })?.custom_prompt || '');
245+
const initialPrompt = (profile as { custom_prompt?: string })?.custom_prompt || '';
246+
setCustomPrompt(initialPrompt);
247+
savedCustomPromptRef.current = initialPrompt;
230248
isInitialized.current = true;
231249
setTimeout(() => resetTracking(), 100);
232250
}
@@ -239,7 +257,7 @@ const ProfileInfoTabContainer = ({
239257
}, 100);
240258
return () => clearTimeout(timer);
241259
}
242-
}, [displayName, username, email, customPrompt, resetTracking, profile]);
260+
}, [displayName, username, email, resetTracking, profile]);
243261

244262
useEffect(() => {
245263
if (profileUpdateError) {
@@ -341,7 +359,10 @@ const ProfileInfoTabContainer = ({
341359
username={username}
342360
setUsername={setUsername}
343361
customPrompt={customPrompt}
344-
setCustomPrompt={setCustomPrompt}
362+
setCustomPrompt={handleCustomPromptChange}
363+
isPromptDirty={isPromptDirty}
364+
isSavingPrompt={isSavingPrompt}
365+
onSaveCustomPrompt={handleSaveCustomPrompt}
345366
errorProfile={errorProfile}
346367
isErrorProfileQuery={isErrorProfileQuery}
347368
errorProfileQueryMessage={errorProfileQuery?.message}

apps/web/src/features/auth/services/profileApiService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ export const profileApiService = {
259259
igel_modus: profile.igel_modus || false,
260260
beta_features: profile.beta_features || {},
261261
memory_enabled: profile.memory_enabled || false,
262+
custom_prompt: profile.custom_prompt || '',
262263
};
263264

264265
return profileData;

0 commit comments

Comments
 (0)