fix: group selection drop-downs recreation and repopulation logic (#3424)

* fix: include in-memory templates in group participant character list

_getCharacterList() only fetched user templates from the /api/presets/templates
endpoint. When a character was just created in the Character tab, the async
auto-save to the templates API might not have completed by the time the Group
tab loaded its participant dropdown — causing newly created characters to be
missing.

Now also merges the in-memory userTemplates array from presets.js as a
fallback. These are updated as soon as the async save completes (via the
loadUserTemplates callback), so they bridge the gap between character creation
and API persistence.

Fixes #3207

* fix: optimistic userTemplates update on character save

Update the in-memory userTemplates array immediately when saveCustomPreset()
succeeds, before the fire-and-forget templates API POST completes. This
bridges the timing gap where _getCharacterList() calls getUserTemplates()
and gets stale data because loadUserTemplates() hasn't been triggered yet.

* test: verify group participant dropdown merges in-memory templates

Source-level guards for the #3207 fix:
- group.js imports and calls getUserTemplates() to merge in-memory templates
- presets.js exports getUserTemplates and does optimistic in-memory update on save

5 tests ensuring the fix can't be silently reverted.

* fix: generate client-side id for optimistic update, return shallow copy from getUserTemplates

1. New characters now get a 'user-<hex>' id immediately on save, matching
   the server's convention (uuid.uuid4().hex[:8]). Previously the id was ''
   which the merge guard in _getCharacterList filtered as falsy.

2. getUserTemplates() now returns [...userTemplates] so callers cannot
   accidentally mutate module state.

* fix(group.js): fix selection drop-downs behavior

- add an identifier to the selection drop-downs
  based on what type it is.
- fix behavior of continuously adding a row
  when a user clicks the "Group" tab button.
- fix behavior of not repopulating existing
  selection drop-downs whenever a user
  clicks the "Group" tab button.

* fix(#3207): remove duplicate of latest persona

- fix the duplication of the latest persona
  or character being shown in selection
  drop-downs.
- remove unnecessary blocks of code in
  `_getCharacterList()`
- add functionality to show error toast if saving
  a preset template/character fails.
- add functionality to revert optimistic update
  of preset template/character if saving fails.

* chore(group.js,preset.js): fix test & format errors

remove trailing whitespaces in lines 230 and 232
in /static/group.js

add back the expected syntax from
tests/test_group_character_dropdown.py

* fix(presets.js,group.js): fix runtime errors

as stated in a comment by @alteixeira20,
runtime errors exist for the applied fixes.

fixes:

- missing ending `]`
  querySelectorAll("select.preset-input[data-selection-type=character")
  in `group.js`
- spelling error in `modelSelection.vale` in `group.js`
- fix the ordering logic error in optimistic rollback where `Object.assign` is called first before the clone happens in `saveCustomPreset` in `presets.js`.
- add tests for the cloning logic bug with the same format as previous tests by checking the order of LOC in `tests/test_group_character_dropdown.py`.

---------

Co-authored-by: michaelxer <michaelxer@users.noreply.github.com>
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
This commit is contained in:
Hinode
2026-06-26 20:35:25 +08:00
committed by GitHub
parent f5200ec45b
commit a5b60a34ee
3 changed files with 204 additions and 20 deletions
+48 -7
View File
@@ -830,15 +830,48 @@ export async function saveCustomPreset(showToast, showError) {
const _selVal = document.getElementById('char-template-select')?.value || '';
const isBuiltinPreset = PROMPT_TEMPLATES.some(t => t.isPreset && (t.name === name || t.name === _selVal));
const saveName = isBuiltinPreset ? null : (name || null);
if (saveName) {
fetch(`${API_BASE}/api/presets/templates`, {
method: 'POST',
const _existing = userTemplates.find(t => t.name === saveName);
let clone;
const _entry = {
id: _existing && _existing.id
|| 'user-' + Math.random().toString(16).slice(2, 10),
name: saveName,
// use ?? since it's more semantic for null-coalescing
system_prompt: system_prompt ?? '',
temperature: config.temperature,
max_tokens: config.max_tokens,
}
const ENDPOINT = `${API_BASE}/api/presets/templates`;
// Optimistically update the in-memory templates list by @michaelxer
if (_existing) {
// slow but works for now
clone = JSON.parse(JSON.stringify(_existing));
Object.assign(_existing, _entry);
} else {
userTemplates.push(_entry);
}
fetch(ENDPOINT, {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: (userTemplates.find(t => t.name === saveName) || {}).id || '',
name: saveName, system_prompt, temperature: config.temperature, max_tokens: config.max_tokens,
}),
}).then(r => { if (r.ok) loadUserTemplates(); }).catch(() => {});
body: JSON.stringify(_entry)
}).then((r) => {
if (r.ok) {
loadUserTemplates();
}
}).catch(() => {
if (clone) {
Object.assign(_existing, clone);
}
if (showError) {
showError(_isInjectStart ? "Something went wrong. Saved prompt has been undone." : "Something went wrong. Saved persona has been undone.");
}
});
}
if (showToast) {
@@ -883,6 +916,13 @@ export function getAllPresets() {
return presets;
}
/**
* Get the in-memory user templates list (may be stale; call loadUserTemplates first if freshness matters).
*/
export function getUserTemplates() {
return [...userTemplates];
}
/**
* Get the character name (if set)
*/
@@ -1099,6 +1139,7 @@ const presetsModule = {
getSelectedPreset,
getPreset,
getAllPresets,
getUserTemplates,
getCharacterName,
onSessionSwitch,
isPersistentChat,