From d8cb4a768be4f885756cd99e76d500bd7e43dfc8 Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 16 Oct 2025 17:33:50 -0700 Subject: [PATCH] feat: implement message examples usage from character cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 -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 --- src-tauri/src/lib.rs | 131 ++++++++++++++++++++++++++++++++++++++++++- src/index.html | 31 +++++++++- src/main.js | 29 ++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4ff9b45..0f5edf6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -291,6 +291,10 @@ struct RoleplaySettings { recursion_depth: usize, // Max depth for recursive World Info activation (default 3) #[serde(default)] active_preset_id: Option, // 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 { @@ -301,6 +305,10 @@ fn default_scan_depth() -> usize { 20 } +fn default_examples_position() -> String { + "after_system".to_string() // Insert examples after system prompt, before history +} + fn default_recursion_depth() -> usize { 3 } @@ -318,6 +326,8 @@ impl Default for RoleplaySettings { scan_depth: default_scan_depth(), recursion_depth: default_recursion_depth(), 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 } +// Parse mes_example field from character card into Message objects +fn parse_message_examples( + mes_example: &str, + character: &Character, + settings: &RoleplaySettings, +) -> Vec { + let mut examples = Vec::new(); + + // Split by tag to get individual example blocks + let blocks: Vec<&str> = mes_example + .split("") + .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 fn build_roleplay_context( character: &Character, @@ -1398,6 +1476,29 @@ fn build_api_messages( let mut api_messages = vec![Message::new_user(enhanced_system_prompt)]; 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 for msg in &history.messages { 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) } +#[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] fn update_recursion_depth( character_id: String, @@ -2797,6 +2910,7 @@ struct TokenBreakdown { persona: usize, world_info: usize, authors_note: usize, + message_examples: usize, message_history: usize, current_input: usize, estimated_max_tokens: usize, @@ -2887,6 +3001,19 @@ fn get_token_count(character_id: Option, current_input: String) -> Resul 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 let mut history_tokens = 0; for msg in &history.messages { @@ -2898,7 +3025,7 @@ fn get_token_count(character_id: Option, current_input: String) -> Resul // Calculate total 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) let estimated_max_tokens = if total < 12000 { 4096 } else { 16384 - total }; @@ -2910,6 +3037,7 @@ fn get_token_count(character_id: Option, current_input: String) -> Resul persona: persona_tokens, world_info: world_info_tokens, authors_note: authors_note_tokens, + message_examples: examples_tokens, message_history: history_tokens, current_input: input_tokens, estimated_max_tokens, @@ -3211,6 +3339,7 @@ pub fn run() { update_roleplay_depths, update_authors_note, update_persona, + update_examples_settings, update_recursion_depth, get_presets, get_preset, diff --git a/src/index.html b/src/index.html index 61c8d00..13e5b71 100644 --- a/src/index.html +++ b/src/index.html @@ -153,9 +153,34 @@ Enable Author's Note - + + +
+ +

+ Use character card's message examples to teach the AI the character's voice and style. +

+ +
+
+ + +

+ Where to inject examples in the context. After system prompt works best for most models. +

+
+ @@ -671,6 +696,10 @@ Author's Note: 0 +
+ Message Examples: + 0 +
Message History: 0 diff --git a/src/main.js b/src/main.js index 7550d2c..df17aa5 100644 --- a/src/main.js +++ b/src/main.js @@ -1561,6 +1561,7 @@ function setupAppControls() { document.getElementById('add-worldinfo-btn').addEventListener('click', handleAddWorldInfoEntry); document.getElementById('save-authors-note-btn').addEventListener('click', handleSaveAuthorsNote); document.getElementById('save-persona-btn').addEventListener('click', handleSavePersona); + document.getElementById('save-examples-btn').addEventListener('click', handleSaveExamples); // Setup recursion depth change handler document.getElementById('recursion-depth').addEventListener('change', handleRecursionDepthChange); @@ -1622,6 +1623,7 @@ async function updateTokenCount() { document.getElementById('token-persona').textContent = tokenData.persona; document.getElementById('token-worldinfo').textContent = tokenData.world_info; 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-input').textContent = tokenData.current_input; 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-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 await loadPresets(); } 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 async function handleRecursionDepthChange() { if (!currentCharacter) return;