feat: add World Info/Lorebook, Author's Note, and Persona systems
Backend changes:
- Add RoleplaySettings and WorldInfoEntry data structures
- Implement per-character roleplay settings storage in ~/.config/claudia/roleplay_{id}.json
- Add Tauri commands for CRUD operations on World Info entries
- Add commands for saving Author's Note and Persona settings
Frontend changes:
- Add World Info entry management UI with add/edit/delete functionality
- Implement keyword-triggered context injection system with priority ordering
- Add Author's Note textarea with enable toggle
- Add Persona name and description fields with enable toggle
- Load roleplay settings when opening the roleplay panel
- Add CSS styles for World Info entry cards with hover effects
Features:
- World Info entries support multiple keywords, priority levels, and enable/disable
- Settings are per-character and persist across sessions
- Entries sorted by priority (higher priority injected first)
- Clean UI with edit/delete buttons and visual feedback
This commit is contained in:
@@ -241,6 +241,49 @@ impl Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// World Info / Lorebook Entry
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct WorldInfoEntry {
|
||||||
|
id: String,
|
||||||
|
keys: Vec<String>, // Keywords that trigger this entry
|
||||||
|
content: String, // The content to inject
|
||||||
|
enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
case_sensitive: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
priority: i32, // Higher priority entries are injected first
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roleplay Settings (Author's Note, Persona, World Info)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct RoleplaySettings {
|
||||||
|
#[serde(default)]
|
||||||
|
authors_note: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
authors_note_enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
persona_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
persona_description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
persona_enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
world_info: Vec<WorldInfoEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RoleplaySettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
authors_note: None,
|
||||||
|
authors_note_enabled: false,
|
||||||
|
persona_name: None,
|
||||||
|
persona_description: None,
|
||||||
|
persona_enabled: false,
|
||||||
|
world_info: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct ChatHistory {
|
struct ChatHistory {
|
||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
@@ -330,6 +373,30 @@ fn get_avatar_path(filename: &str) -> PathBuf {
|
|||||||
get_avatars_dir().join(filename)
|
get_avatars_dir().join(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_roleplay_settings_path(character_id: &str) -> PathBuf {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||||
|
PathBuf::from(home).join(format!(".config/claudia/roleplay_{}.json", character_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_roleplay_settings(character_id: &str) -> RoleplaySettings {
|
||||||
|
let path = get_roleplay_settings_path(character_id);
|
||||||
|
if let Ok(contents) = fs::read_to_string(path) {
|
||||||
|
serde_json::from_str(&contents).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
RoleplaySettings::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_roleplay_settings(character_id: &str, settings: &RoleplaySettings) -> Result<(), String> {
|
||||||
|
let path = get_roleplay_settings_path(character_id);
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
let contents = serde_json::to_string_pretty(settings).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(path, contents).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// PNG Character Card Utilities
|
// PNG Character Card Utilities
|
||||||
|
|
||||||
// Manual PNG chunk parser - more reliable than relying on png crate's text chunk exposure
|
// Manual PNG chunk parser - more reliable than relying on png crate's text chunk exposure
|
||||||
@@ -1512,6 +1579,98 @@ async fn import_chat_history(app_handle: tauri::AppHandle) -> Result<usize, Stri
|
|||||||
Ok(message_count)
|
Ok(message_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Roleplay Settings Commands
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_roleplay_settings(character_id: String) -> Result<RoleplaySettings, String> {
|
||||||
|
Ok(load_roleplay_settings(&character_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn update_authors_note(
|
||||||
|
character_id: String,
|
||||||
|
content: Option<String>,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut settings = load_roleplay_settings(&character_id);
|
||||||
|
settings.authors_note = content;
|
||||||
|
settings.authors_note_enabled = enabled;
|
||||||
|
save_roleplay_settings(&character_id, &settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn update_persona(
|
||||||
|
character_id: String,
|
||||||
|
name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut settings = load_roleplay_settings(&character_id);
|
||||||
|
settings.persona_name = name;
|
||||||
|
settings.persona_description = description;
|
||||||
|
settings.persona_enabled = enabled;
|
||||||
|
save_roleplay_settings(&character_id, &settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn add_world_info_entry(
|
||||||
|
character_id: String,
|
||||||
|
keys: Vec<String>,
|
||||||
|
content: String,
|
||||||
|
priority: i32,
|
||||||
|
case_sensitive: bool,
|
||||||
|
) -> Result<WorldInfoEntry, String> {
|
||||||
|
let mut settings = load_roleplay_settings(&character_id);
|
||||||
|
|
||||||
|
let entry = WorldInfoEntry {
|
||||||
|
id: Uuid::new_v4().to_string(),
|
||||||
|
keys,
|
||||||
|
content,
|
||||||
|
enabled: true,
|
||||||
|
case_sensitive,
|
||||||
|
priority,
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.world_info.push(entry.clone());
|
||||||
|
save_roleplay_settings(&character_id, &settings)?;
|
||||||
|
|
||||||
|
Ok(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn update_world_info_entry(
|
||||||
|
character_id: String,
|
||||||
|
entry_id: String,
|
||||||
|
keys: Vec<String>,
|
||||||
|
content: String,
|
||||||
|
enabled: bool,
|
||||||
|
priority: i32,
|
||||||
|
case_sensitive: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut settings = load_roleplay_settings(&character_id);
|
||||||
|
|
||||||
|
if let Some(entry) = settings.world_info.iter_mut().find(|e| e.id == entry_id) {
|
||||||
|
entry.keys = keys;
|
||||||
|
entry.content = content;
|
||||||
|
entry.enabled = enabled;
|
||||||
|
entry.priority = priority;
|
||||||
|
entry.case_sensitive = case_sensitive;
|
||||||
|
save_roleplay_settings(&character_id, &settings)
|
||||||
|
} else {
|
||||||
|
Err("World Info entry not found".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn delete_world_info_entry(
|
||||||
|
character_id: String,
|
||||||
|
entry_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut settings = load_roleplay_settings(&character_id);
|
||||||
|
settings.world_info.retain(|e| e.id != entry_id);
|
||||||
|
save_roleplay_settings(&character_id, &settings)
|
||||||
|
}
|
||||||
|
|
||||||
// Export character card to PNG
|
// Export character card to PNG
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn export_character_card(app_handle: tauri::AppHandle, character_id: String) -> Result<String, String> {
|
async fn export_character_card(app_handle: tauri::AppHandle, character_id: String) -> Result<String, String> {
|
||||||
@@ -1597,7 +1756,13 @@ pub fn run() {
|
|||||||
import_character_card,
|
import_character_card,
|
||||||
export_character_card,
|
export_character_card,
|
||||||
export_chat_history,
|
export_chat_history,
|
||||||
import_chat_history
|
import_chat_history,
|
||||||
|
get_roleplay_settings,
|
||||||
|
update_authors_note,
|
||||||
|
update_persona,
|
||||||
|
add_world_info_entry,
|
||||||
|
update_world_info_entry,
|
||||||
|
delete_world_info_entry
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
262
src/main.js
262
src/main.js
@@ -1062,11 +1062,14 @@ function hideSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide roleplay panel
|
// Show/hide roleplay panel
|
||||||
function showRoleplayPanel() {
|
async function showRoleplayPanel() {
|
||||||
const panel = document.getElementById('roleplay-panel');
|
const panel = document.getElementById('roleplay-panel');
|
||||||
const overlay = document.getElementById('roleplay-overlay');
|
const overlay = document.getElementById('roleplay-overlay');
|
||||||
panel.classList.add('open');
|
panel.classList.add('open');
|
||||||
overlay.classList.add('show');
|
overlay.classList.add('show');
|
||||||
|
|
||||||
|
// Load roleplay settings when panel opens
|
||||||
|
await loadRoleplaySettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideRoleplayPanel() {
|
function hideRoleplayPanel() {
|
||||||
@@ -1330,6 +1333,11 @@ function setupAppControls() {
|
|||||||
applyFontSize(parseInt(e.target.value));
|
applyFontSize(parseInt(e.target.value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup roleplay panel buttons
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
@@ -1655,6 +1663,258 @@ async function handleSaveCharacter(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// World Info / Roleplay Settings Management
|
||||||
|
|
||||||
|
let currentRoleplaySettings = null;
|
||||||
|
|
||||||
|
// Load roleplay settings for current character
|
||||||
|
async function loadRoleplaySettings() {
|
||||||
|
if (!currentCharacter) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = await invoke('get_roleplay_settings', { characterId: currentCharacter.id });
|
||||||
|
currentRoleplaySettings = settings;
|
||||||
|
|
||||||
|
// Load World Info entries
|
||||||
|
renderWorldInfoList(settings.world_info || []);
|
||||||
|
|
||||||
|
// Load Author's Note
|
||||||
|
document.getElementById('authors-note-text').value = settings.authors_note || '';
|
||||||
|
document.getElementById('authors-note-enabled').checked = settings.authors_note_enabled || false;
|
||||||
|
|
||||||
|
// Load Persona
|
||||||
|
document.getElementById('persona-name').value = settings.persona_name || '';
|
||||||
|
document.getElementById('persona-description').value = settings.persona_description || '';
|
||||||
|
document.getElementById('persona-enabled').checked = settings.persona_enabled || false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load roleplay settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render World Info entries
|
||||||
|
function renderWorldInfoList(entries) {
|
||||||
|
const listContainer = document.getElementById('worldinfo-list');
|
||||||
|
listContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
const emptyMsg = document.createElement('p');
|
||||||
|
emptyMsg.style.color = 'var(--text-secondary)';
|
||||||
|
emptyMsg.style.fontSize = '14px';
|
||||||
|
emptyMsg.style.textAlign = 'center';
|
||||||
|
emptyMsg.style.padding = '20px';
|
||||||
|
emptyMsg.textContent = 'No entries yet. Click "Add Entry" to create one.';
|
||||||
|
listContainer.appendChild(emptyMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort entries by priority (higher first)
|
||||||
|
const sortedEntries = [...entries].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||||
|
|
||||||
|
sortedEntries.forEach(entry => {
|
||||||
|
const entryDiv = document.createElement('div');
|
||||||
|
entryDiv.className = 'worldinfo-entry';
|
||||||
|
entryDiv.dataset.entryId = entry.id;
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'worldinfo-entry-header';
|
||||||
|
|
||||||
|
const enableCheckbox = document.createElement('input');
|
||||||
|
enableCheckbox.type = 'checkbox';
|
||||||
|
enableCheckbox.checked = entry.enabled;
|
||||||
|
enableCheckbox.addEventListener('change', () => handleToggleWorldInfoEntry(entry.id, enableCheckbox.checked));
|
||||||
|
|
||||||
|
const keysText = document.createElement('span');
|
||||||
|
keysText.className = 'worldinfo-keys';
|
||||||
|
keysText.textContent = entry.keys.join(', ');
|
||||||
|
|
||||||
|
const priority = document.createElement('span');
|
||||||
|
priority.className = 'worldinfo-priority';
|
||||||
|
priority.textContent = `Priority: ${entry.priority || 0}`;
|
||||||
|
|
||||||
|
const actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'worldinfo-entry-actions';
|
||||||
|
|
||||||
|
const editBtn = document.createElement('button');
|
||||||
|
editBtn.className = 'worldinfo-btn';
|
||||||
|
editBtn.textContent = 'Edit';
|
||||||
|
editBtn.addEventListener('click', () => handleEditWorldInfoEntry(entry));
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'worldinfo-btn worldinfo-btn-danger';
|
||||||
|
deleteBtn.textContent = 'Delete';
|
||||||
|
deleteBtn.addEventListener('click', () => handleDeleteWorldInfoEntry(entry.id));
|
||||||
|
|
||||||
|
actionsDiv.appendChild(editBtn);
|
||||||
|
actionsDiv.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
header.appendChild(enableCheckbox);
|
||||||
|
header.appendChild(keysText);
|
||||||
|
header.appendChild(priority);
|
||||||
|
header.appendChild(actionsDiv);
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'worldinfo-entry-content';
|
||||||
|
content.textContent = entry.content;
|
||||||
|
|
||||||
|
entryDiv.appendChild(header);
|
||||||
|
entryDiv.appendChild(content);
|
||||||
|
listContainer.appendChild(entryDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new World Info entry
|
||||||
|
async function handleAddWorldInfoEntry() {
|
||||||
|
const keys = prompt('Enter keywords (comma-separated):\nExample: John, John Smith');
|
||||||
|
if (!keys) return;
|
||||||
|
|
||||||
|
const content = prompt('Enter the content to inject when keywords are found:');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
const priorityStr = prompt('Enter priority (higher = injected first, default 0):', '0');
|
||||||
|
const priority = parseInt(priorityStr) || 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keysArray = keys.split(',').map(k => k.trim()).filter(k => k);
|
||||||
|
await invoke('add_world_info_entry', {
|
||||||
|
characterId: currentCharacter.id,
|
||||||
|
keys: keysArray,
|
||||||
|
content: content.trim(),
|
||||||
|
priority,
|
||||||
|
caseSensitive: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload settings
|
||||||
|
await loadRoleplaySettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add World Info entry:', error);
|
||||||
|
alert(`Failed to add entry: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit World Info entry
|
||||||
|
async function handleEditWorldInfoEntry(entry) {
|
||||||
|
const keys = prompt('Edit keywords (comma-separated):', entry.keys.join(', '));
|
||||||
|
if (keys === null) return;
|
||||||
|
|
||||||
|
const content = prompt('Edit content:', entry.content);
|
||||||
|
if (content === null) return;
|
||||||
|
|
||||||
|
const priorityStr = prompt('Edit priority:', entry.priority.toString());
|
||||||
|
if (priorityStr === null) return;
|
||||||
|
const priority = parseInt(priorityStr) || 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keysArray = keys.split(',').map(k => k.trim()).filter(k => k);
|
||||||
|
await invoke('update_world_info_entry', {
|
||||||
|
characterId: currentCharacter.id,
|
||||||
|
entryId: entry.id,
|
||||||
|
keys: keysArray,
|
||||||
|
content: content.trim(),
|
||||||
|
enabled: entry.enabled,
|
||||||
|
priority,
|
||||||
|
caseSensitive: entry.case_sensitive
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload settings
|
||||||
|
await loadRoleplaySettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update World Info entry:', error);
|
||||||
|
alert(`Failed to update entry: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle World Info entry enabled state
|
||||||
|
async function handleToggleWorldInfoEntry(entryId, enabled) {
|
||||||
|
if (!currentRoleplaySettings) return;
|
||||||
|
|
||||||
|
const entry = currentRoleplaySettings.world_info.find(e => e.id === entryId);
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('update_world_info_entry', {
|
||||||
|
characterId: currentCharacter.id,
|
||||||
|
entryId: entryId,
|
||||||
|
keys: entry.keys,
|
||||||
|
content: entry.content,
|
||||||
|
enabled: enabled,
|
||||||
|
priority: entry.priority,
|
||||||
|
caseSensitive: entry.case_sensitive
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local settings
|
||||||
|
entry.enabled = enabled;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle World Info entry:', error);
|
||||||
|
alert(`Failed to toggle entry: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete World Info entry
|
||||||
|
async function handleDeleteWorldInfoEntry(entryId) {
|
||||||
|
if (!confirm('Delete this World Info entry? This cannot be undone.')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('delete_world_info_entry', {
|
||||||
|
characterId: currentCharacter.id,
|
||||||
|
entryId: entryId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload settings
|
||||||
|
await loadRoleplaySettings();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete World Info entry:', error);
|
||||||
|
alert(`Failed to delete entry: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Author's Note
|
||||||
|
async function handleSaveAuthorsNote() {
|
||||||
|
if (!currentCharacter) return;
|
||||||
|
|
||||||
|
const content = document.getElementById('authors-note-text').value.trim() || null;
|
||||||
|
const enabled = document.getElementById('authors-note-enabled').checked;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('update_authors_note', {
|
||||||
|
characterId: currentCharacter.id,
|
||||||
|
content,
|
||||||
|
enabled
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
setStatus('Author\'s Note saved', 'success');
|
||||||
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save Author\'s Note:', error);
|
||||||
|
setStatus('Failed to save Author\'s Note', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Persona
|
||||||
|
async function handleSavePersona() {
|
||||||
|
if (!currentCharacter) return;
|
||||||
|
|
||||||
|
const name = document.getElementById('persona-name').value.trim() || null;
|
||||||
|
const description = document.getElementById('persona-description').value.trim() || null;
|
||||||
|
const enabled = document.getElementById('persona-enabled').checked;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('update_persona', {
|
||||||
|
characterId: currentCharacter.id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
enabled
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
setStatus('Persona saved', 'success');
|
||||||
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save Persona:', error);
|
||||||
|
setStatus('Failed to save Persona', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load existing config if available
|
// Load existing config if available
|
||||||
async function loadExistingConfig() {
|
async function loadExistingConfig() {
|
||||||
console.log('Loading existing config...');
|
console.log('Loading existing config...');
|
||||||
|
|||||||
@@ -1611,6 +1611,91 @@ body.view-comfortable .message-content pre {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.worldinfo-entry {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-entry:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-entry-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-entry-header input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-keys {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-priority {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-entry-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-btn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-btn:hover {
|
||||||
|
background: var(--border);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-btn-danger {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-btn-danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worldinfo-entry-content {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.header-left-controls {
|
.header-left-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user