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:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
29
src/main.js
29
src/main.js
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user