feat: implement message examples usage from character cards

Add support for using mes_example field from character cards to teach the AI the character's voice and writing style. Examples are parsed, processed with template variable replacement, and injected into the context at a configurable position.

Backend changes:
- Extended RoleplaySettings with examples_enabled and examples_position fields
- Implemented parse_message_examples() to parse <START>-delimited example blocks
- Added example injection in build_api_messages() with position control
- Integrated examples into token counter with accurate counting
- Created update_examples_settings command for saving settings

Frontend changes:
- Added Message Examples UI controls in Author's Note tab
- Checkbox to enable/disable examples
- Dropdown to select injection position (after_system/before_history)
- Save button with success/error feedback
- Token breakdown now shows examples token count
- Settings load/save integrated with roleplay panel

Message examples help the AI understand character personality, speaking patterns, and response style by providing concrete examples of how the character should respond.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-16 17:33:50 -07:00
parent b9230772ed
commit d8cb4a768b
3 changed files with 189 additions and 2 deletions

View File

@@ -291,6 +291,10 @@ struct RoleplaySettings {
recursion_depth: usize, // Max depth for recursive World Info activation (default 3) recursion_depth: usize, // Max depth for recursive World Info activation (default 3)
#[serde(default)] #[serde(default)]
active_preset_id: Option<String>, // Selected prompt preset for this character active_preset_id: Option<String>, // Selected prompt preset for this character
#[serde(default)]
examples_enabled: bool, // Whether to include message examples from character card
#[serde(default = "default_examples_position")]
examples_position: String, // Where to insert examples: "after_system" or "before_history"
} }
fn default_authors_note_depth() -> usize { fn default_authors_note_depth() -> usize {
@@ -301,6 +305,10 @@ fn default_scan_depth() -> usize {
20 20
} }
fn default_examples_position() -> String {
"after_system".to_string() // Insert examples after system prompt, before history
}
fn default_recursion_depth() -> usize { fn default_recursion_depth() -> usize {
3 3
} }
@@ -318,6 +326,8 @@ impl Default for RoleplaySettings {
scan_depth: default_scan_depth(), scan_depth: default_scan_depth(),
recursion_depth: default_recursion_depth(), recursion_depth: default_recursion_depth(),
active_preset_id: None, // No preset selected by default active_preset_id: None, // No preset selected by default
examples_enabled: false, // Message examples disabled by default
examples_position: default_examples_position(), // After system prompt by default
} }
} }
} }
@@ -1310,6 +1320,74 @@ fn replace_template_variables(
result result
} }
// Parse mes_example field from character card into Message objects
fn parse_message_examples(
mes_example: &str,
character: &Character,
settings: &RoleplaySettings,
) -> Vec<Message> {
let mut examples = Vec::new();
// Split by <START> tag to get individual example blocks
let blocks: Vec<&str> = mes_example
.split("<START>")
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
for block in blocks {
// Process each line in the block
for line in block.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
// Replace template variables
let processed_line = replace_template_variables(line, character, settings);
// Determine role based on prefix ({{user}}: or {{char}}:)
// After replacement, it will be the actual names
let user_name = if settings.persona_enabled {
settings.persona_name.as_deref().unwrap_or("User")
} else {
"User"
};
if processed_line.starts_with(&format!("{}:", user_name)) {
// User message
let content = processed_line
.trim_start_matches(&format!("{}:", user_name))
.trim()
.to_string();
examples.push(Message::new_user(content));
} else if processed_line.starts_with(&format!("{}:", character.name)) {
// Assistant message
let content = processed_line
.trim_start_matches(&format!("{}:", character.name))
.trim()
.to_string();
examples.push(Message::new_assistant(content));
} else if processed_line.contains(':') {
// Fallback: split on first colon
let parts: Vec<&str> = processed_line.splitn(2, ':').collect();
if parts.len() == 2 {
let speaker = parts[0].trim();
let content = parts[1].trim().to_string();
if speaker == user_name {
examples.push(Message::new_user(content));
} else {
examples.push(Message::new_assistant(content));
}
}
}
}
}
examples
}
// Build injected context from roleplay settings // Build injected context from roleplay settings
fn build_roleplay_context( fn build_roleplay_context(
character: &Character, character: &Character,
@@ -1398,6 +1476,29 @@ fn build_api_messages(
let mut api_messages = vec![Message::new_user(enhanced_system_prompt)]; let mut api_messages = vec![Message::new_user(enhanced_system_prompt)];
api_messages[0].role = "system".to_string(); api_messages[0].role = "system".to_string();
// Insert message examples if enabled
if roleplay_settings.examples_enabled {
if let Some(ref mes_example) = character.mes_example {
if !mes_example.is_empty() {
let examples = parse_message_examples(mes_example, character, roleplay_settings);
// Insert examples based on position setting
match roleplay_settings.examples_position.as_str() {
"after_system" => {
// Insert right after system message (position 1)
for (i, example) in examples.into_iter().enumerate() {
api_messages.insert(1 + i, example);
}
}
"before_history" | _ => {
// Insert at end (before history gets added)
api_messages.extend(examples);
}
}
}
}
}
// Add history messages with current swipe content // Add history messages with current swipe content
for msg in &history.messages { for msg in &history.messages {
let mut api_msg = Message::new_user(msg.get_content().to_string()); let mut api_msg = Message::new_user(msg.get_content().to_string());
@@ -2630,6 +2731,18 @@ fn update_persona(
save_roleplay_settings(&character_id, &settings) save_roleplay_settings(&character_id, &settings)
} }
#[tauri::command]
fn update_examples_settings(
character_id: String,
enabled: bool,
position: String,
) -> Result<(), String> {
let mut settings = load_roleplay_settings(&character_id);
settings.examples_enabled = enabled;
settings.examples_position = position;
save_roleplay_settings(&character_id, &settings)
}
#[tauri::command] #[tauri::command]
fn update_recursion_depth( fn update_recursion_depth(
character_id: String, character_id: String,
@@ -2797,6 +2910,7 @@ struct TokenBreakdown {
persona: usize, persona: usize,
world_info: usize, world_info: usize,
authors_note: usize, authors_note: usize,
message_examples: usize,
message_history: usize, message_history: usize,
current_input: usize, current_input: usize,
estimated_max_tokens: usize, estimated_max_tokens: usize,
@@ -2887,6 +3001,19 @@ fn get_token_count(character_id: Option<String>, current_input: String) -> Resul
0 0
}; };
// Count message examples
let mut examples_tokens = 0;
if roleplay_settings.examples_enabled {
if let Some(ref mes_example) = character.mes_example {
if !mes_example.is_empty() {
let examples = parse_message_examples(mes_example, &character, &roleplay_settings);
for example in examples {
examples_tokens += count_tokens(example.get_content());
}
}
}
}
// Count message history // Count message history
let mut history_tokens = 0; let mut history_tokens = 0;
for msg in &history.messages { for msg in &history.messages {
@@ -2898,7 +3025,7 @@ fn get_token_count(character_id: Option<String>, current_input: String) -> Resul
// Calculate total // Calculate total
let total = system_prompt_tokens + preset_tokens + persona_tokens + world_info_tokens + let total = system_prompt_tokens + preset_tokens + persona_tokens + world_info_tokens +
authors_note_tokens + history_tokens + input_tokens; authors_note_tokens + examples_tokens + history_tokens + input_tokens;
// Estimate remaining tokens for response (assuming 16k context with 4k max response) // Estimate remaining tokens for response (assuming 16k context with 4k max response)
let estimated_max_tokens = if total < 12000 { 4096 } else { 16384 - total }; let estimated_max_tokens = if total < 12000 { 4096 } else { 16384 - total };
@@ -2910,6 +3037,7 @@ fn get_token_count(character_id: Option<String>, current_input: String) -> Resul
persona: persona_tokens, persona: persona_tokens,
world_info: world_info_tokens, world_info: world_info_tokens,
authors_note: authors_note_tokens, authors_note: authors_note_tokens,
message_examples: examples_tokens,
message_history: history_tokens, message_history: history_tokens,
current_input: input_tokens, current_input: input_tokens,
estimated_max_tokens, estimated_max_tokens,
@@ -3211,6 +3339,7 @@ pub fn run() {
update_roleplay_depths, update_roleplay_depths,
update_authors_note, update_authors_note,
update_persona, update_persona,
update_examples_settings,
update_recursion_depth, update_recursion_depth,
get_presets, get_presets,
get_preset, get_preset,

View File

@@ -153,9 +153,34 @@
Enable Author's Note Enable Author's Note
</label> </label>
</div> </div>
<button type="button" id="save-authors-note-btn" class="btn-primary" style="width: 100%;"> <button type="button" id="save-authors-note-btn" class="btn-primary" style="width: 100%; margin-bottom: 20px;">
Save Author's Note Save Author's Note
</button> </button>
<!-- Message Examples Section -->
<div class="form-group" style="border-top: 1px solid var(--border); padding-top: 16px;">
<label>Message Examples</label>
<p style="color: var(--text-secondary); font-size: 12px; margin-bottom: 8px;">
Use character card's message examples to teach the AI the character's voice and style.
</p>
<label>
<input type="checkbox" id="examples-enabled" />
Enable Message Examples
</label>
</div>
<div class="form-group">
<label for="examples-position">Examples Position</label>
<select id="examples-position" style="width: 100%;">
<option value="after_system">After System Prompt (Recommended)</option>
<option value="before_history">Before Message History</option>
</select>
<p style="color: var(--text-secondary); font-size: 11px; margin-top: 4px;">
Where to inject examples in the context. After system prompt works best for most models.
</p>
</div>
<button type="button" id="save-examples-btn" class="btn-primary" style="width: 100%;">
Save Examples Settings
</button>
</div> </div>
</div> </div>
@@ -671,6 +696,10 @@
<span>Author's Note:</span> <span>Author's Note:</span>
<span id="token-authorsnote">0</span> <span id="token-authorsnote">0</span>
</div> </div>
<div class="token-breakdown-item">
<span>Message Examples:</span>
<span id="token-examples">0</span>
</div>
<div class="token-breakdown-item"> <div class="token-breakdown-item">
<span>Message History:</span> <span>Message History:</span>
<span id="token-history">0</span> <span id="token-history">0</span>

View File

@@ -1561,6 +1561,7 @@ function setupAppControls() {
document.getElementById('add-worldinfo-btn').addEventListener('click', handleAddWorldInfoEntry); document.getElementById('add-worldinfo-btn').addEventListener('click', handleAddWorldInfoEntry);
document.getElementById('save-authors-note-btn').addEventListener('click', handleSaveAuthorsNote); document.getElementById('save-authors-note-btn').addEventListener('click', handleSaveAuthorsNote);
document.getElementById('save-persona-btn').addEventListener('click', handleSavePersona); document.getElementById('save-persona-btn').addEventListener('click', handleSavePersona);
document.getElementById('save-examples-btn').addEventListener('click', handleSaveExamples);
// Setup recursion depth change handler // Setup recursion depth change handler
document.getElementById('recursion-depth').addEventListener('change', handleRecursionDepthChange); document.getElementById('recursion-depth').addEventListener('change', handleRecursionDepthChange);
@@ -1622,6 +1623,7 @@ async function updateTokenCount() {
document.getElementById('token-persona').textContent = tokenData.persona; document.getElementById('token-persona').textContent = tokenData.persona;
document.getElementById('token-worldinfo').textContent = tokenData.world_info; document.getElementById('token-worldinfo').textContent = tokenData.world_info;
document.getElementById('token-authorsnote').textContent = tokenData.authors_note; document.getElementById('token-authorsnote').textContent = tokenData.authors_note;
document.getElementById('token-examples').textContent = tokenData.message_examples;
document.getElementById('token-history').textContent = tokenData.message_history; document.getElementById('token-history').textContent = tokenData.message_history;
document.getElementById('token-input').textContent = tokenData.current_input; document.getElementById('token-input').textContent = tokenData.current_input;
document.getElementById('token-total-detail').textContent = tokenData.total; document.getElementById('token-total-detail').textContent = tokenData.total;
@@ -2012,6 +2014,10 @@ async function loadRoleplaySettings() {
document.getElementById('persona-description').value = settings.persona_description || ''; document.getElementById('persona-description').value = settings.persona_description || '';
document.getElementById('persona-enabled').checked = settings.persona_enabled || false; document.getElementById('persona-enabled').checked = settings.persona_enabled || false;
// Load Message Examples
document.getElementById('examples-enabled').checked = settings.examples_enabled || false;
document.getElementById('examples-position').value = settings.examples_position || 'after_system';
// Load Presets // Load Presets
await loadPresets(); await loadPresets();
} catch (error) { } catch (error) {
@@ -2243,6 +2249,29 @@ async function handleSavePersona() {
} }
} }
// Save Message Examples Settings
async function handleSaveExamples() {
if (!currentCharacter) return;
const enabled = document.getElementById('examples-enabled').checked;
const position = document.getElementById('examples-position').value;
try {
await invoke('update_examples_settings', {
characterId: currentCharacter.id,
enabled,
position
});
// Show success message
setStatus('Message Examples settings saved', 'success');
setTimeout(() => setStatus('Ready'), 2000);
} catch (error) {
console.error('Failed to save Message Examples settings:', error);
setStatus('Failed to save Message Examples settings', 'error');
}
}
// Handle recursion depth change // Handle recursion depth change
async function handleRecursionDepthChange() { async function handleRecursionDepthChange() {
if (!currentCharacter) return; if (!currentCharacter) return;