diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c5b4b9..e71b04a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
+## [1.13.2] — 2026-07-06
+
+### Fixed
+- **Character table view rendered as stacked blocks, not a table** — table rows reused the `.lib-char-card` class (for its click/voice/export wiring) whose `display:flex;flex-direction:column` turned every `
` into a flex column, stacking its cells vertically instead of laying them out side by side. Reset to `display:table-row` and stripped the card-specific chrome (background, border, shadow) that leaked in with it.
+
+### Changed
+- **One Generate button per prompt box** instead of a single "Generate" that always regenerated all four (Voice Design, Character Image, SillyTavern, Concept Art) at once. The server endpoint now accepts a `fields` filter and runs one independent LLM call per requested field — clicking one button no longer burns tokens on the other three, and each call's smaller JSON further reduces truncation risk.
+- **Sort dropdown for the character list** (Library → Characters/Cast) — Role/Alphabet/Number of lines/Gender/Voice assigned, persisted like the Cards/Table toggle.
+
+---
+
## [1.13.1] — 2026-07-06
### Added
diff --git a/VERSION b/VERSION
index b50dd27..61ce01b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.13.1
+1.13.2
diff --git a/routes/conversation.py b/routes/conversation.py
index 1839726..cdbc747 100644
--- a/routes/conversation.py
+++ b/routes/conversation.py
@@ -870,46 +870,55 @@ async def character_generate_prompts(request: Request):
)
user = f"BOOK: {book or 'unspecified'}\n\nCHARACTER PROFILE:\n{profile or name}\n\n"
- # Split into two independent calls instead of one four-field JSON object.
- # Even at a 4096-token ceiling, the model reliably ran out of budget before
- # reaching the LAST two fields (silly_tavern_prompt, concept_art_prompt come
- # after voice_design_prompt/image_prompt in the JSON) — truncation always
- # cost the same two fields. Each pair now gets its own full token budget,
- # so a squeeze in one pair can't cost the other pair anything, and the two
- # calls run concurrently so this isn't slower than the single-call version.
- field_groups = [
- (
- ("voice_design_prompt", "image_prompt"),
+ # One independent call per field (not one four-field JSON object, nor even
+ # the two-field pairing this used briefly) — the UI now has a Generate
+ # button per prompt box, so a click on just one must not also burn tokens
+ # regenerating the other three. Singling them out also further shrinks
+ # each response, since a four-field JSON object reliably truncated the
+ # LAST fields regardless of the token ceiling.
+ all_groups = {
+ "voice_design_prompt": (
preamble
- + "Produce exactly these two fields:\n"
+ + "Produce exactly this field:\n"
"- voice_design_prompt: an English prompt for Qwen3 TTS Voice Design (15-45 words, one paragraph, "
"no markdown). Cover: apparent age, gender/androgyny if inferable, pitch, timbre/texture, pace, "
"accent or register, emotional baseline, and suitability for audiobook dialogue delivery. Do not "
- "mention plot events — describe only how the voice should SOUND.\n"
+ "mention plot events — describe only how the voice should SOUND.\n\n"
+ 'Respond with STRICT JSON only: {"voice_design_prompt":""}/no-think'
+ ),
+ "image_prompt": (
+ preamble
+ + "Produce exactly this field:\n"
"- image_prompt: a detailed English image-generation prompt for a character profile picture/portrait. "
"Include face and expression typical of this character, age impression, build, hair/eyes/skin if known, "
"clothing, signature tools/weapons/props, an environment typical for them, mood, and an art style "
"(e.g. 'detailed digital painting, dramatic lighting'). One dense paragraph, comma-separated descriptors "
"are fine.\n\n"
- 'Respond with STRICT JSON only: {"voice_design_prompt":"","image_prompt":""}/no-think',
+ 'Respond with STRICT JSON only: {"image_prompt":""}/no-think'
),
- (
- ("silly_tavern_prompt", "concept_art_prompt"),
+ "silly_tavern_prompt": (
preamble
- + "Produce exactly these two fields:\n"
+ + "Produce exactly this field:\n"
"- silly_tavern_prompt: character-card content for SillyTavern, formatted as labelled sections on their "
"own lines: 'Description:' (physical + personality summary), 'Personality:' (a compact trait list), "
"'Scenario:' (the situation/setting they're typically found in), 'First message:' (one in-character "
"greeting line in their own voice/speech pattern), and 'Example dialogue:' (2-3 short in-character "
- "lines showing their manner of speech). Keep each section a few lines at most.\n"
+ "lines showing their manner of speech). Keep each section a few lines at most.\n\n"
+ 'Respond with STRICT JSON only: {"silly_tavern_prompt":""}/no-think'
+ ),
+ "concept_art_prompt": (
+ preamble
+ + "Produce exactly this field:\n"
"- concept_art_prompt: an English prompt for a character CONCEPT SHEET (not a single portrait) — "
"a turnaround/reference sheet with multiple views and expressions: front view, side or back view, "
"2-3 facial expressions, and a close-up of a signature prop/costume detail, all on one clean sheet, "
"in a character-design-sheet art style (e.g. 'character turnaround, model sheet, flat lighting, "
"white background').\n\n"
- 'Respond with STRICT JSON only: {"silly_tavern_prompt":"","concept_art_prompt":""}/no-think',
+ 'Respond with STRICT JSON only: {"concept_art_prompt":""}/no-think'
),
- ]
+ }
+ requested = [f for f in (data.get("fields") or []) if f in all_groups]
+ field_groups = [((f,), all_groups[f]) for f in (requested or all_groups.keys())]
def _call_group(fields: tuple, system: str) -> dict:
payload: dict = {
@@ -918,7 +927,7 @@ async def character_generate_prompts(request: Request):
{"role": "user", "content": user + f"Generate the {' and '.join(fields)} now."},
],
"temperature": 0.7,
- "max_tokens": 2560,
+ "max_tokens": 1536,
}
if model:
payload["model"] = model
diff --git a/static/index.html b/static/index.html
index 90c2afd..17fdb72 100644
--- a/static/index.html
+++ b/static/index.html
@@ -10,7 +10,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
-
+