feat: add v2 character card import/export

- Expanded Character struct with all v2 spec fields (description, scenario, mes_example, post_history_instructions, alternate_greetings, character_book, tags, creator, character_version, creator_notes, extensions)
- Created CharacterCardV2 serialization structs following spec at github.com/malfoyslastname/character-card-spec-v2
- Implemented PNG metadata utilities:
  * read_character_card_from_png() - extracts and decodes character data from PNG tEXt chunks
  * write_character_card_to_png() - embeds character data into PNG files
  * create_placeholder_png() - generates gradient placeholder images for avatarless characters
- Added Tauri commands:
  * import_character_card - opens file picker, imports PNG with automatic name conflict handling
  * export_character_card - exports character as v2 PNG card with embedded metadata
- Added Import/Export buttons to character settings UI
- Full backward compatibility with existing characters using serde defaults
- Added dependencies: png 0.17, base64 0.21, image 0.24
This commit is contained in:
2025-10-14 08:19:57 -07:00
parent f82ec6f6a8
commit f31e3fb28a
6 changed files with 632 additions and 3 deletions

View File

@@ -971,6 +971,8 @@ function setupAppControls() {
});
document.getElementById('upload-avatar-btn').addEventListener('click', handleAvatarUpload);
document.getElementById('remove-avatar-btn').addEventListener('click', handleAvatarRemove);
document.getElementById('import-character-btn').addEventListener('click', handleImportCharacter);
document.getElementById('export-character-btn').addEventListener('click', handleExportCharacter);
}
// Keyboard shortcuts
@@ -1077,6 +1079,51 @@ async function handleDeleteCharacter() {
}
}
// Handle character card import
async function handleImportCharacter() {
const characterMsg = document.getElementById('character-message');
try {
const importedCharacter = await invoke('import_character_card');
characterMsg.textContent = `Successfully imported ${importedCharacter.name}!`;
characterMsg.className = 'validation-message success';
// Reload characters and switch to the imported one
await loadCharacters();
await loadCharacterSettings();
setTimeout(() => {
characterMsg.style.display = 'none';
}, 3000);
} catch (error) {
console.error('Failed to import character:', error);
if (error && !error.toString().includes('No file selected') && !error.toString().includes('cancelled')) {
characterMsg.textContent = `Failed to import: ${error}`;
characterMsg.className = 'validation-message error';
}
}
}
// Handle character card export
async function handleExportCharacter() {
const characterMsg = document.getElementById('character-message');
try {
const characterId = document.getElementById('character-settings-select').value;
const outputPath = await invoke('export_character_card', { characterId });
characterMsg.textContent = `Successfully exported to ${outputPath}`;
characterMsg.className = 'validation-message success';
setTimeout(() => {
characterMsg.style.display = 'none';
}, 3000);
} catch (error) {
console.error('Failed to export character:', error);
if (error && !error.toString().includes('cancelled')) {
characterMsg.textContent = `Failed to export: ${error}`;
characterMsg.className = 'validation-message error';
}
}
}
// Load chat history
async function loadChatHistory() {
try {