diff --git a/README.md b/README.md
index d2ba5d2..4545c38 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,7 @@ Config stored in `~/.config/claudia/config.json`
- **Left/Right Arrow** - Navigate between response alternatives
- **Escape** - Close panels/modals, cancel editing
- **Ctrl+K** - Focus message input
+- **Ctrl+P** - Open command palette (quick access to all actions)
- **Ctrl+/** - Toggle Roleplay Tools panel
## Roadmap
diff --git a/ROADMAP.md b/ROADMAP.md
index 2981753..d81c2a6 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -19,11 +19,12 @@
- Enhanced Message Controls (delete, pin, hide, continue, regenerate any message)
- Token Counter (real-time display with per-section breakdown)
- Message Examples (character card examples injected into context)
+- Chat Branching/Checkpoints (create, switch, delete, rename branches from any message)
-### 🎯 Current Focus: Advanced Chat Management
-**Next Up:** Implementing Chat Branching/Checkpoints to enable non-linear conversation exploration with the ability to save conversation states, create branches from any point, and switch between different conversation paths.
+### 🎯 Current Focus: Quality of Life & Polish
+**Next Up:** Implementing high-impact QoL features to reduce friction and improve user experience - starting with Toast Notifications, Command Palette, Auto-save, Drag & Drop, and Chat Search.
-**Recent Completion:** Message Examples - character card message examples are now parsed, processed with template variable replacement, and injected into context at configurable positions to teach the AI the character's voice and writing style.
+**Recent Completion:** Chat Branching/Checkpoints - Full conversation branching system allowing users to create and explore alternate conversation paths from any message point. Each branch maintains its own complete message history with a branch manager modal for easy navigation.
## Phase 1: Core Roleplay Infrastructure (High Priority)
**Goal: Enable basic roleplay-focused prompt engineering**
@@ -87,13 +88,14 @@
## Phase 3: Advanced Chat Management (Medium Priority)
**Goal: Non-linear conversation control**
-### 1. Chat Branching/Checkpoints
-- [ ] Save conversation state at any message
-- [ ] Create branches from any point
-- [ ] Switch between branches
-- [ ] Visual branch indicator in UI
-- [ ] Branch naming and organization
-- [ ] Delete/merge branches
+### 1. Chat Branching/Checkpoints ✅
+- [x] Save conversation state at any message
+- [x] Create branches from any point
+- [x] Switch between branches
+- [x] Visual branch indicator in UI
+- [x] Branch naming and organization
+- [x] Delete branches
+- [ ] Merge branches (deferred - nice to have)
**Why Important:** Roleplay often involves exploring "what if" scenarios. Branching lets you explore different conversation paths without losing previous progress.
@@ -252,6 +254,151 @@
**Why Important:** Better UI means less friction and more immersion in roleplay.
+## Phase 8: Quality of Life & Polish (High Priority)
+**Goal: Reduce friction, improve feedback, and enhance overall user experience**
+
+### 1. Toast Notification System
+- [ ] Create toast component (bottom-right positioning)
+- [ ] Success/error/info/warning variants
+- [ ] Auto-dismiss with configurable timeout
+- [ ] Queue multiple toasts
+- [ ] Hook into all major actions (save, delete, import, export, etc.)
+
+**Why Important:** Users currently have no immediate feedback when actions succeed or fail. Toasts provide instant visual confirmation without blocking workflow.
+
+### 2. Command Palette
+- [ ] Ctrl+P to open command palette modal
+- [ ] Fuzzy search for all actions
+- [ ] Keyboard navigation (arrow keys, enter, escape)
+- [ ] Recent/frequent actions at top
+- [ ] Show keyboard shortcuts in results
+- [ ] Categories (Chat, Character, Settings, etc.)
+
+**Why Important:** Power users want keyboard-first workflow. Command palette dramatically speeds up common actions without memorizing shortcuts.
+
+### 3. Auto-save & Recovery
+- [ ] Auto-save unsent message in input field
+- [ ] Restore unsent message after app restart
+- [ ] Draft system for in-progress edits
+- [ ] Session recovery (restore scroll position, open panels)
+- [ ] Crash recovery with last known state
+
+**Why Important:** Losing work due to crashes or accidental closes is extremely frustrating. Auto-save provides a safety net for all user work.
+
+### 4. Drag & Drop Support
+- [ ] Drag character card PNGs to import
+- [ ] Drag lorebook JSON files to import
+- [ ] Drag chat history JSON to import
+- [ ] Drag images to set as character avatar
+- [ ] Drop zone overlay with visual feedback
+- [ ] Support for multiple file drops
+
+**Why Important:** Drag & drop feels natural and is much faster than navigate-click-select workflow. Modern desktop apps are expected to support this.
+
+### 5. Search in Chat History
+- [ ] Ctrl+F to open search bar
+- [ ] Highlight all matches in messages
+- [ ] Navigate between results (prev/next buttons)
+- [ ] Case-insensitive search
+- [ ] Search counter (e.g., "3 of 42 matches")
+- [ ] Clear search and restore view
+
+**Why Important:** Long roleplay sessions can span hundreds of messages. Finding specific content without search is tedious and time-consuming.
+
+### 6. Context Menus (Right-Click)
+- [ ] Right-click messages for actions (edit, delete, regenerate, branch, copy)
+- [ ] Right-click character dropdown for quick actions
+- [ ] Right-click World Info entries for edit/delete
+- [ ] Right-click in message input for paste/clear/templates
+- [ ] Context-aware menu items
+
+**Why Important:** Right-click is muscle memory for desktop users. Faster than hovering to reveal action buttons.
+
+### 7. Better Feedback & Confirmations
+- [ ] Confirmation dialogs for destructive actions (delete character, clear chat)
+- [ ] Loading spinners for API calls
+- [ ] Progress bars for file imports
+- [ ] "Saving..." / "Saved" indicators
+- [ ] Success messages for completed actions
+
+**Why Important:** Users should never wonder if an action succeeded or is still processing. Clear feedback prevents confusion and repeated clicks.
+
+### 8. Undo/Redo System
+- [ ] Undo message edit (Ctrl+Z)
+- [ ] Undo message delete
+- [ ] Undo character field changes
+- [ ] Undo World Info changes
+- [ ] Action history panel (optional)
+- [ ] Redo support (Ctrl+Shift+Z)
+
+**Why Important:** Mistakes happen. An undo system provides a safety net and encourages experimentation without fear of losing work.
+
+### 9. Settings Search
+- [ ] Search bar at top of settings panel
+- [ ] Fuzzy search across all setting names and descriptions
+- [ ] Highlight matching settings
+- [ ] Collapse/expand sections based on matches
+- [ ] "Recently changed" section
+
+**Why Important:** With 22+ features, finding specific settings is tedious. Search makes configuration much faster.
+
+### 10. Character Management Enhancements
+- [ ] Recent characters quick-switch dropdown
+- [ ] Character search/filter by name or tags
+- [ ] Character folders/categories
+- [ ] Duplicate character (as template)
+- [ ] Favorite/star characters
+- [ ] Sort options (name, date created, last used)
+
+**Why Important:** Managing 10+ characters becomes messy. Better organization tools scale with user's character collection.
+
+### 11. Enhanced Keyboard Support
+- [ ] Full keyboard navigation in all modals (Tab, Arrow keys, Enter)
+- [ ] Escape to close any open panel/modal
+- [ ] Vim-style navigation mode (optional, j/k for scroll)
+- [ ] Keyboard shortcut hints on hover
+- [ ] Focus indicators for keyboard navigation
+
+**Why Important:** Keyboard navigation should work everywhere. Current implementation is inconsistent across different UI sections.
+
+### 12. Export/Share Enhancements
+- [ ] Export conversation as formatted HTML
+- [ ] Export conversation as formatted PDF
+- [ ] Export as markdown with proper formatting
+- [ ] Copy conversation to clipboard (formatted)
+- [ ] Export individual messages
+
+**Why Important:** Users want to share and archive conversations in readable formats, not just JSON.
+
+### 13. Accessibility Improvements
+- [ ] ARIA labels for all interactive elements
+- [ ] Screen reader support
+- [ ] High contrast mode option
+- [ ] Larger click targets option (accessibility mode)
+- [ ] Reduced motion mode (respect prefers-reduced-motion)
+- [ ] Focus indicators for keyboard navigation
+
+**Why Important:** Accessibility makes the app usable for everyone, including users with disabilities. It's also often legally required.
+
+### 14. Better Visual Feedback
+- [ ] Smooth transitions for panel open/close
+- [ ] Hover states for all interactive elements
+- [ ] Active state indicators (focused panel)
+- [ ] Better empty states with helpful text
+- [ ] Skeleton loaders for content loading
+- [ ] Micro-animations for actions (delete, save, etc.)
+
+**Why Important:** Visual polish makes the app feel responsive and professional. Small animations provide context for state changes.
+
+### 15. Smart Defaults & Templates
+- [ ] Scenario templates (fantasy RPG, sci-fi, modern, etc.)
+- [ ] Pre-filled World Info templates
+- [ ] Character card templates
+- [ ] Quick-start wizard for new users
+- [ ] Import from popular character repositories
+
+**Why Important:** Reduces friction for new users and speeds up common tasks. Templates provide starting points for customization.
+
## Implementation Priority Ranking
### Must-Have for Basic Roleplay:
diff --git a/src/index.html b/src/index.html
index df4009a..7240d45 100644
--- a/src/index.html
+++ b/src/index.html
@@ -733,6 +733,33 @@
+
+
diff --git a/src/main.js b/src/main.js
index e609e26..c27cfd0 100644
--- a/src/main.js
+++ b/src/main.js
@@ -146,6 +146,511 @@ function loadSavedTheme() {
applyTheme(savedTheme);
}
+// Toast Notification System
+const toastQueue = [];
+let toastContainer;
+
+function showToast(options) {
+ const {
+ type = 'info',
+ title,
+ message,
+ duration = 3000,
+ dismissible = true
+ } = options;
+
+ if (!toastContainer) {
+ toastContainer = document.getElementById('toast-container');
+ }
+
+ const toast = document.createElement('div');
+ toast.className = `toast ${type}`;
+
+ // Icon based on type
+ const icons = {
+ success: `
+
+ `,
+ error: `
+
+
+ `,
+ warning: `
+
+
+ `,
+ info: `
+
+
+ `
+ };
+
+ toast.innerHTML = `
+
${icons[type]}
+
+ ${title ? `
${title}
` : ''}
+ ${message ? `
${message}
` : ''}
+
+ ${dismissible ? `
+
+
+
+ ` : ''}
+ ${duration > 0 ? `
` : ''}
+ `;
+
+ toastContainer.appendChild(toast);
+
+ // Dismiss on close button click
+ if (dismissible) {
+ const closeBtn = toast.querySelector('.toast-close');
+ closeBtn.addEventListener('click', () => {
+ dismissToast(toast);
+ });
+ }
+
+ // Auto-dismiss
+ if (duration > 0) {
+ setTimeout(() => {
+ dismissToast(toast);
+ }, duration);
+ }
+
+ // Click to dismiss (anywhere on toast)
+ toast.addEventListener('click', (e) => {
+ if (e.target !== toast.querySelector('.toast-close') && e.target.closest('.toast-close') === null) {
+ dismissToast(toast);
+ }
+ });
+
+ return toast;
+}
+
+function dismissToast(toast) {
+ if (!toast || toast.classList.contains('removing')) return;
+
+ toast.classList.add('removing');
+ setTimeout(() => {
+ if (toast.parentNode) {
+ toast.parentNode.removeChild(toast);
+ }
+ }, 300);
+}
+
+// Convenience functions
+function showSuccess(title, message, duration) {
+ return showToast({ type: 'success', title, message, duration });
+}
+
+function showError(title, message, duration) {
+ return showToast({ type: 'error', title, message, duration });
+}
+
+function showWarning(title, message, duration) {
+ return showToast({ type: 'warning', title, message, duration });
+}
+
+function showInfo(title, message, duration) {
+ return showToast({ type: 'info', title, message, duration });
+}
+
+// Command Palette System
+let commandPaletteModal;
+let commandPaletteInput;
+let commandPaletteResults;
+let selectedCommandIndex = 0;
+let filteredCommands = [];
+
+// Define all available commands
+const commands = [
+ // Chat actions
+ {
+ id: 'clear-chat',
+ title: 'Clear Conversation',
+ description: 'Clear all messages in the current conversation',
+ category: 'Chat',
+ icon: `
`,
+ action: () => clearHistory(),
+ keywords: ['delete', 'remove', 'reset']
+ },
+ {
+ id: 'export-chat',
+ title: 'Export Conversation',
+ description: 'Save conversation to a JSON file',
+ category: 'Chat',
+ icon: `
`,
+ action: () => exportChatHistory(),
+ keywords: ['save', 'download', 'backup']
+ },
+ {
+ id: 'import-chat',
+ title: 'Import Conversation',
+ description: 'Load conversation from a JSON file',
+ category: 'Chat',
+ icon: `
`,
+ action: () => importChatHistory(),
+ keywords: ['load', 'restore', 'open']
+ },
+ // Character actions
+ {
+ id: 'new-character',
+ title: 'New Character',
+ description: 'Create a new character',
+ category: 'Characters',
+ icon: `
`,
+ action: () => handleNewCharacter(),
+ shortcut: ['Ctrl', 'N'],
+ keywords: ['create', 'add']
+ },
+ {
+ id: 'import-character',
+ title: 'Import Character Card',
+ description: 'Import a character from PNG card',
+ category: 'Characters',
+ icon: `
`,
+ action: () => handleImportCharacter(),
+ keywords: ['load', 'v2', 'card', 'png']
+ },
+ {
+ id: 'export-character',
+ title: 'Export Character Card',
+ description: 'Export current character as PNG card',
+ category: 'Characters',
+ icon: `
`,
+ action: () => handleExportCharacter(),
+ keywords: ['save', 'v2', 'card', 'png']
+ },
+ {
+ id: 'delete-character',
+ title: 'Delete Character',
+ description: 'Delete the current character',
+ category: 'Characters',
+ icon: `
`,
+ action: () => handleDeleteCharacter(),
+ keywords: ['remove']
+ },
+ // Settings actions
+ {
+ id: 'open-settings',
+ title: 'Open Settings',
+ description: 'Open the settings panel',
+ category: 'Settings',
+ icon: `
`,
+ action: () => showSettings(),
+ keywords: ['preferences', 'config', 'api']
+ },
+ {
+ id: 'open-roleplay-tools',
+ title: 'Open Roleplay Tools',
+ description: 'Open the roleplay tools panel',
+ category: 'Settings',
+ icon: `
`,
+ action: () => showRoleplayPanel(),
+ shortcut: ['Ctrl', '/'],
+ keywords: ['world info', 'lorebook', 'persona', 'preset']
+ },
+ // Focus actions
+ {
+ id: 'focus-input',
+ title: 'Focus Message Input',
+ description: 'Move cursor to the message input',
+ category: 'Navigation',
+ icon: `
`,
+ action: () => {
+ if (messageInput) messageInput.focus();
+ closeCommandPalette();
+ },
+ shortcut: ['Ctrl', 'K'],
+ keywords: ['cursor', 'type']
+ },
+ // Theme actions
+ {
+ id: 'theme-dark',
+ title: 'Switch to Dark Theme',
+ description: 'Change theme to dark mode',
+ category: 'Appearance',
+ icon: `
`,
+ action: () => {
+ applyTheme('dark');
+ document.getElementById('theme-select').value = 'dark';
+ closeCommandPalette();
+ showSuccess('Theme Changed', 'Switched to Dark theme');
+ },
+ keywords: ['color', 'style']
+ },
+ {
+ id: 'theme-light',
+ title: 'Switch to Light Theme',
+ description: 'Change theme to light mode',
+ category: 'Appearance',
+ icon: `
`,
+ action: () => {
+ applyTheme('light');
+ document.getElementById('theme-select').value = 'light';
+ closeCommandPalette();
+ showSuccess('Theme Changed', 'Switched to Light theme');
+ },
+ keywords: ['color', 'style']
+ }
+];
+
+function openCommandPalette() {
+ if (!commandPaletteModal) {
+ commandPaletteModal = document.getElementById('command-palette-modal');
+ commandPaletteInput = document.getElementById('command-palette-input');
+ commandPaletteResults = document.getElementById('command-palette-results');
+
+ // Overlay click to close
+ commandPaletteModal.querySelector('.command-palette-overlay').addEventListener('click', closeCommandPalette);
+
+ // Input event listener for filtering
+ commandPaletteInput.addEventListener('input', (e) => {
+ filterCommands(e.target.value);
+ });
+ }
+
+ commandPaletteModal.style.display = 'flex';
+ commandPaletteInput.value = '';
+ selectedCommandIndex = 0;
+
+ // Show all commands initially
+ filterCommands('');
+
+ // Focus the input after a brief delay to ensure modal is visible
+ setTimeout(() => {
+ commandPaletteInput.focus();
+ }, 50);
+}
+
+function closeCommandPalette() {
+ if (commandPaletteModal) {
+ commandPaletteModal.style.display = 'none';
+ commandPaletteInput.value = '';
+ }
+}
+
+function filterCommands(searchTerm) {
+ const term = searchTerm.toLowerCase().trim();
+
+ if (term === '') {
+ filteredCommands = [...commands];
+ } else {
+ filteredCommands = commands.filter(cmd => {
+ const titleMatch = cmd.title.toLowerCase().includes(term);
+ const descMatch = cmd.description.toLowerCase().includes(term);
+ const categoryMatch = cmd.category.toLowerCase().includes(term);
+ const keywordMatch = cmd.keywords && cmd.keywords.some(k => k.includes(term));
+
+ return titleMatch || descMatch || categoryMatch || keywordMatch;
+ });
+ }
+
+ selectedCommandIndex = 0;
+ renderCommandResults();
+}
+
+function renderCommandResults() {
+ if (filteredCommands.length === 0) {
+ commandPaletteResults.innerHTML = `
+
+
+
+
+
+
+
No commands found
+
+ `;
+ return;
+ }
+
+ // Group commands by category
+ const grouped = {};
+ filteredCommands.forEach(cmd => {
+ if (!grouped[cmd.category]) {
+ grouped[cmd.category] = [];
+ }
+ grouped[cmd.category].push(cmd);
+ });
+
+ let html = '';
+ Object.keys(grouped).forEach((category, catIndex) => {
+ html += `
${category}
`;
+
+ grouped[category].forEach((cmd, cmdIndex) => {
+ const globalIndex = filteredCommands.indexOf(cmd);
+ const isSelected = globalIndex === selectedCommandIndex;
+
+ html += `
+
+
${cmd.icon}
+
+
${cmd.title}
+
${cmd.description}
+
+ ${cmd.shortcut ? `
+
+ ${cmd.shortcut.map(key => `${key} `).join('')}
+
+ ` : ''}
+
+ `;
+ });
+ });
+
+ commandPaletteResults.innerHTML = html;
+
+ // Add click handlers
+ commandPaletteResults.querySelectorAll('.command-item').forEach(item => {
+ item.addEventListener('click', () => {
+ const index = parseInt(item.dataset.index);
+ executeCommand(filteredCommands[index]);
+ });
+ });
+
+ // Scroll selected item into view
+ const selectedItem = commandPaletteResults.querySelector('.command-item.selected');
+ if (selectedItem) {
+ selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+ }
+}
+
+function executeCommand(command) {
+ if (command && command.action) {
+ closeCommandPalette();
+ command.action();
+ }
+}
+
+function handleCommandPaletteKeydown(e) {
+ if (!commandPaletteModal || commandPaletteModal.style.display === 'none') return;
+
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ closeCommandPalette();
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ selectedCommandIndex = Math.min(selectedCommandIndex + 1, filteredCommands.length - 1);
+ renderCommandResults();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ selectedCommandIndex = Math.max(selectedCommandIndex - 1, 0);
+ renderCommandResults();
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ if (filteredCommands.length > 0) {
+ executeCommand(filteredCommands[selectedCommandIndex]);
+ }
+ }
+}
+
+// Auto-save & Recovery System
+let autoSaveTimeout;
+const AUTO_SAVE_DELAY = 1000; // Save after 1 second of inactivity
+
+function getAutoSaveKey() {
+ if (!currentCharacter) return null;
+ return `claudia-draft-${currentCharacter.id}`;
+}
+
+function autoSaveMessageInput() {
+ const key = getAutoSaveKey();
+ if (!key || !messageInput) return;
+
+ const content = messageInput.value.trim();
+
+ if (content === '') {
+ // Clear the draft if input is empty
+ localStorage.removeItem(key);
+ } else {
+ // Save the draft
+ const draft = {
+ content,
+ timestamp: Date.now(),
+ characterId: currentCharacter.id,
+ characterName: currentCharacter.name
+ };
+ localStorage.setItem(key, JSON.stringify(draft));
+ }
+}
+
+function loadAutoSavedDraft() {
+ const key = getAutoSaveKey();
+ if (!key || !messageInput) return;
+
+ try {
+ const savedDraft = localStorage.getItem(key);
+ if (!savedDraft) return;
+
+ const draft = JSON.parse(savedDraft);
+
+ // Check if draft is not too old (older than 7 days)
+ const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
+ const age = Date.now() - draft.timestamp;
+
+ if (age > maxAge) {
+ // Draft is too old, remove it
+ localStorage.removeItem(key);
+ return;
+ }
+
+ // Restore the draft
+ if (draft.content && draft.content.trim() !== '') {
+ messageInput.value = draft.content;
+
+ // Auto-resize the textarea
+ messageInput.style.height = 'auto';
+ messageInput.style.height = messageInput.scrollHeight + 'px';
+
+ // Show notification
+ const draftAge = formatDraftAge(age);
+ showInfo('Draft Recovered', `Unsent message from ${draftAge} ago has been restored.`, 4000);
+ }
+ } catch (error) {
+ console.error('Failed to load auto-saved draft:', error);
+ }
+}
+
+function clearAutoSavedDraft() {
+ const key = getAutoSaveKey();
+ if (!key) return;
+ localStorage.removeItem(key);
+}
+
+function formatDraftAge(milliseconds) {
+ const seconds = Math.floor(milliseconds / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+
+ if (days > 0) return `${days} day${days > 1 ? 's' : ''}`;
+ if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''}`;
+ if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''}`;
+ return 'a few seconds';
+}
+
+function setupAutoSave() {
+ if (!messageInput) return;
+
+ // Auto-save on input with debouncing
+ messageInput.addEventListener('input', () => {
+ // Clear existing timeout
+ if (autoSaveTimeout) {
+ clearTimeout(autoSaveTimeout);
+ }
+
+ // Set new timeout to save after delay
+ autoSaveTimeout = setTimeout(() => {
+ autoSaveMessageInput();
+ }, AUTO_SAVE_DELAY);
+ });
+
+ // Also save on blur (when user clicks away)
+ messageInput.addEventListener('blur', () => {
+ autoSaveMessageInput();
+ });
+}
+
// Apply view mode
function applyViewMode(mode) {
const body = document.body;
@@ -206,14 +711,14 @@ async function exportChatHistory() {
try {
setStatus('Exporting chat...', 'default');
const filePath = await invoke('export_chat_history');
- setStatus('Chat exported successfully!', 'success');
- setTimeout(() => setStatus('Ready'), 2000);
+ setStatus('Ready');
+ showSuccess('Chat Exported', `Successfully exported to ${filePath}`, 4000);
console.log('Chat exported to:', filePath);
} catch (error) {
console.error('Export failed:', error);
if (error && !error.toString().includes('cancelled')) {
- setStatus(`Export failed: ${error}`, 'error');
- setTimeout(() => setStatus('Ready'), 3000);
+ setStatus('Ready');
+ showError('Export Failed', `Failed to export chat: ${error}`);
} else {
setStatus('Ready');
}
@@ -229,15 +734,15 @@ async function importChatHistory() {
// Reload the chat history
await loadChatHistory();
- setStatus(`Imported ${messageCount} messages successfully!`, 'success');
- setTimeout(() => setStatus('Ready'), 2000);
+ setStatus('Ready');
+ showSuccess('Chat Imported', `Successfully imported ${messageCount} messages!`);
} catch (error) {
console.error('Import failed:', error);
if (error === 'No file selected' || error.toString().includes('cancelled')) {
setStatus('Ready');
} else {
- setStatus(`Import failed: ${error}`, 'error');
- setTimeout(() => setStatus('Ready'), 3000);
+ setStatus('Ready');
+ showError('Import Failed', `Failed to import chat: ${error}`);
}
}
}
@@ -1024,11 +1529,10 @@ async function handleDeleteMessage(messageDiv) {
await invoke('delete_message_at_index', { messageIndex });
messageDiv.remove();
await updateTokenCount();
- setStatus('Message deleted', 'success');
- setTimeout(() => setStatus('Ready'), 2000);
+ showSuccess('Message Deleted', 'The message has been deleted successfully.');
} catch (error) {
console.error('Failed to delete message:', error);
- setStatus(`Delete failed: ${error}`, 'error');
+ showError('Delete Failed', `Failed to delete message: ${error}`);
}
}
@@ -1055,13 +1559,13 @@ async function handleCreateBranch(messageDiv) {
branchName: branchName.trim()
});
- setStatus(`Created branch: ${branch.name}`, 'success');
+ showSuccess('Branch Created', `Created new branch: ${branch.name}`);
// Switch to the new branch
await handleSwitchBranch(branch.id);
} catch (error) {
console.error('Failed to create branch:', error);
- setStatus(`Branch creation failed: ${error}`, 'error');
+ showError('Branch Creation Failed', `Failed to create branch: ${error}`);
}
}
@@ -1502,6 +2006,9 @@ async function handleSubmit(e) {
messageInput.value = '';
autoResize(messageInput);
+ // Clear the auto-saved draft since message is being sent
+ clearAutoSavedDraft();
+
await sendMessage(message);
}
@@ -1986,6 +2493,9 @@ async function loadCharacters() {
await loadChatHistory();
await updateBranchIndicator();
+
+ // Load auto-saved draft for this character
+ loadAutoSavedDraft();
} catch (error) {
console.error('Failed to load characters:', error);
addMessage(`Failed to load characters: ${error}`, false);
@@ -2076,63 +2586,51 @@ async function handleNewCharacter() {
// Handle character deletion
async function handleDeleteCharacter() {
if (!currentCharacter || currentCharacter.id === 'default') {
- addMessage('Cannot delete the default character.', false);
+ showWarning('Cannot Delete', 'The default character cannot be deleted.');
return;
}
if (confirm(`Are you sure you want to delete ${currentCharacter.name}? This cannot be undone.`)) {
try {
+ const characterName = currentCharacter.name;
await invoke('delete_character', { characterId: currentCharacter.id });
await loadCharacters();
hideSettings();
+ showSuccess('Character Deleted', `${characterName} has been deleted successfully.`);
} catch (error) {
console.error('Failed to delete character:', error);
- addMessage(`Failed to delete character: ${error}`, false);
+ showError('Delete Failed', `Failed to delete character: ${error}`);
}
}
}
// 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';
+ showSuccess('Character Imported', `${importedCharacter.name} has been imported successfully!`);
// 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';
+ showError('Import Failed', `Failed to import character: ${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);
+ showSuccess('Character Exported', `Successfully exported to ${outputPath}`, 4000);
} 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';
+ showError('Export Failed', `Failed to export character: ${error}`);
}
}
}
@@ -2207,11 +2705,11 @@ async function clearHistory() {
} else {
addMessage('Conversation cleared. Ready to chat.', false, true);
}
- setStatus('History cleared', 'success');
- setTimeout(() => setStatus('Ready'), 2000);
+ setStatus('Ready');
+ showSuccess('History Cleared', 'Conversation history has been cleared.');
} catch (error) {
- setStatus('Failed to clear history', 'error');
- addMessage(`Failed to clear history: ${error}`, false);
+ setStatus('Ready');
+ showError('Clear Failed', `Failed to clear history: ${error}`);
}
}
@@ -2289,8 +2787,7 @@ async function handleSaveCharacter(e) {
const characterMsg = document.getElementById('character-message');
if (!name || !systemPrompt) {
- characterMsg.textContent = 'Name and System Prompt are required';
- characterMsg.className = 'validation-message error';
+ showWarning('Missing Fields', 'Name and System Prompt are required.');
return;
}
@@ -2315,17 +2812,11 @@ async function handleSaveCharacter(e) {
creatorNotes,
avatarPath: pendingAvatarPath
});
- characterMsg.textContent = 'Character saved successfully';
- characterMsg.className = 'validation-message success';
await loadCharacters();
-
- setTimeout(() => {
- characterMsg.style.display = 'none';
- }, 3000);
+ showSuccess('Character Saved', `${name} has been saved successfully.`);
} catch (error) {
- characterMsg.textContent = `Failed to save: ${error}`;
- characterMsg.className = 'validation-message error';
+ showError('Save Failed', `Failed to save character: ${error}`);
} finally {
saveBtn.disabled = false;
saveBtn.classList.remove('loading');
@@ -2759,11 +3250,10 @@ async function handleSaveAuthorsNote() {
updateFeatureBadges();
// Show success message
- setStatus('Author\'s Note saved', 'success');
- setTimeout(() => setStatus('Ready'), 2000);
+ showSuccess('Author\'s Note Saved', 'Your author\'s note has been saved successfully.');
} catch (error) {
console.error('Failed to save Author\'s Note:', error);
- setStatus('Failed to save Author\'s Note', 'error');
+ showError('Save Failed', `Failed to save author's note: ${error}`);
}
}
@@ -2794,11 +3284,10 @@ async function handleSavePersona() {
updateFeatureBadges();
// Show success message
- setStatus('Persona saved', 'success');
- setTimeout(() => setStatus('Ready'), 2000);
+ showSuccess('Persona Saved', 'Your persona has been saved successfully.');
} catch (error) {
console.error('Failed to save Persona:', error);
- setStatus('Failed to save Persona', 'error');
+ showError('Save Failed', `Failed to save persona: ${error}`);
}
}
@@ -2826,11 +3315,10 @@ async function handleSaveExamples() {
updateFeatureBadges();
// Show success message
- setStatus('Message Examples settings saved', 'success');
- setTimeout(() => setStatus('Ready'), 2000);
+ showSuccess('Examples Saved', 'Message examples settings have been saved.');
} catch (error) {
console.error('Failed to save Message Examples settings:', error);
- setStatus('Failed to save Message Examples settings', 'error');
+ showError('Save Failed', `Failed to save message examples settings: ${error}`);
}
}
@@ -3810,6 +4298,9 @@ window.addEventListener('DOMContentLoaded', () => {
// ESC key to close modal
// Global keyboard shortcuts
document.addEventListener('keydown', (e) => {
+ // Handle command palette keydown (for arrow keys, enter, escape)
+ handleCommandPaletteKeydown(e);
+
// Escape key handling
if (e.key === 'Escape') {
// Close new character modal
@@ -3895,6 +4386,13 @@ window.addEventListener('DOMContentLoaded', () => {
return;
}
+ // Ctrl/Cmd + P - Open command palette
+ if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
+ e.preventDefault();
+ openCommandPalette();
+ return;
+ }
+
// Ctrl/Cmd + / - Toggle roleplay panel
if ((e.ctrlKey || e.metaKey) && e.key === '/') {
e.preventDefault();
@@ -3918,5 +4416,8 @@ window.addEventListener('DOMContentLoaded', () => {
loadSavedViewMode();
loadSavedFontSize();
+ // Setup auto-save for message input
+ setupAutoSave();
+
loadExistingConfig();
});
diff --git a/src/styles.css b/src/styles.css
index b5dc17e..8f802d2 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -2181,3 +2181,435 @@ body.view-comfortable .message-content pre {
left: 0;
}
}
+
+/* Toast Notification System */
+.toast-container {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ z-index: 10003;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ pointer-events: none;
+ max-width: 400px;
+}
+
+.toast {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 16px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 300px;
+ max-width: 400px;
+ pointer-events: auto;
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ animation: slideInRight 0.3s ease;
+ transition: all 0.3s ease;
+}
+
+.toast.removing {
+ animation: slideOutRight 0.3s ease forwards;
+}
+
+@keyframes slideInRight {
+ from {
+ transform: translateX(400px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slideOutRight {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ to {
+ transform: translateX(400px);
+ opacity: 0;
+ }
+}
+
+.toast-icon {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.toast-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.toast-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.toast-message {
+ font-size: 13px;
+ color: var(--text-secondary);
+ margin: 0;
+ line-height: 1.4;
+}
+
+.toast-close {
+ width: 20px;
+ height: 20px;
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ padding: 0;
+ flex-shrink: 0;
+}
+
+.toast-close:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: var(--text-primary);
+}
+
+/* Toast Variants */
+.toast.success {
+ border-color: rgba(34, 197, 94, 0.5);
+ background: rgba(34, 197, 94, 0.1);
+}
+
+.toast.success .toast-icon {
+ color: #22c55e;
+}
+
+.toast.success .toast-title {
+ color: #22c55e;
+}
+
+.toast.error {
+ border-color: rgba(239, 68, 68, 0.5);
+ background: rgba(239, 68, 68, 0.1);
+}
+
+.toast.error .toast-icon {
+ color: #ef4444;
+}
+
+.toast.error .toast-title {
+ color: #ef4444;
+}
+
+.toast.warning {
+ border-color: rgba(249, 115, 22, 0.5);
+ background: rgba(249, 115, 22, 0.1);
+}
+
+.toast.warning .toast-icon {
+ color: #f97316;
+}
+
+.toast.warning .toast-title {
+ color: #f97316;
+}
+
+.toast.info {
+ border-color: rgba(99, 102, 241, 0.5);
+ background: rgba(99, 102, 241, 0.1);
+}
+
+.toast.info .toast-icon {
+ color: var(--accent);
+}
+
+.toast.info .toast-title {
+ color: var(--accent);
+}
+
+/* Progress bar for auto-dismiss */
+.toast-progress {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 0 0 12px 12px;
+ overflow: hidden;
+}
+
+.toast-progress-bar {
+ height: 100%;
+ background: currentColor;
+ animation: progressShrink var(--duration) linear forwards;
+}
+
+@keyframes progressShrink {
+ from {
+ width: 100%;
+ }
+ to {
+ width: 0%;
+ }
+}
+
+/* Responsive toast positioning */
+@media (max-width: 600px) {
+ .toast-container {
+ left: 12px;
+ right: 12px;
+ bottom: 12px;
+ max-width: none;
+ }
+
+ .toast {
+ min-width: 0;
+ max-width: none;
+ }
+}
+
+/* Command Palette */
+.command-palette-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 10004;
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: 15vh;
+ animation: fadeIn 0.15s ease;
+}
+
+.command-palette-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ cursor: pointer;
+}
+
+.command-palette-content {
+ position: relative;
+ z-index: 1;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ width: 90%;
+ max-width: 600px;
+ max-height: 60vh;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ animation: slideInDown 0.2s ease;
+}
+
+@keyframes slideInDown {
+ from {
+ transform: translateY(-20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+.command-palette-search {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg-secondary);
+}
+
+.command-palette-search-icon {
+ color: var(--text-secondary);
+ flex-shrink: 0;
+}
+
+.command-palette-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ color: var(--text-primary);
+ font-size: 16px;
+ font-family: inherit;
+ outline: none;
+}
+
+.command-palette-input::placeholder {
+ color: var(--text-secondary);
+}
+
+.command-palette-hint {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 11px;
+ font-family: monospace;
+ color: var(--text-secondary);
+ flex-shrink: 0;
+}
+
+.command-palette-results {
+ overflow-y: auto;
+ max-height: 50vh;
+ padding: 8px;
+}
+
+.command-palette-results::-webkit-scrollbar {
+ width: 8px;
+}
+
+.command-palette-results::-webkit-scrollbar-track {
+ background: var(--bg-primary);
+}
+
+.command-palette-results::-webkit-scrollbar-thumb {
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+}
+
+.command-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ border: 1px solid transparent;
+}
+
+.command-item:hover {
+ background: var(--bg-secondary);
+ border-color: var(--border);
+}
+
+.command-item.selected {
+ background: rgba(99, 102, 241, 0.1);
+ border-color: var(--accent);
+}
+
+.command-item-icon {
+ width: 20px;
+ height: 20px;
+ color: var(--accent);
+ flex-shrink: 0;
+}
+
+.command-item-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.command-item-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+ margin: 0 0 2px 0;
+}
+
+.command-item-description {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.command-item-shortcut {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.command-item-shortcut kbd {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-size: 11px;
+ font-family: monospace;
+ color: var(--text-secondary);
+}
+
+.command-palette-empty {
+ padding: 40px 20px;
+ text-align: center;
+ color: var(--text-secondary);
+}
+
+.command-palette-empty-icon {
+ width: 48px;
+ height: 48px;
+ margin: 0 auto 12px;
+ opacity: 0.5;
+}
+
+.command-palette-empty-text {
+ font-size: 14px;
+ margin: 0;
+}
+
+.command-palette-section {
+ padding: 8px 12px 4px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-top: 8px;
+}
+
+.command-palette-section:first-child {
+ margin-top: 0;
+}
+
+/* Responsive command palette */
+@media (max-width: 600px) {
+ .command-palette-modal {
+ padding-top: 10vh;
+ }
+
+ .command-palette-content {
+ width: 95%;
+ max-height: 70vh;
+ }
+
+ .command-item {
+ padding: 10px;
+ }
+
+ .command-item-shortcut {
+ display: none;
+ }
+}