Compare commits

...

2 Commits

Author SHA1 Message Date
10d95951a3 feat: implement QoL features (toast notifications, command palette, auto-save)
Add three major quality-of-life features to improve user experience:

Toast Notification System:
- Non-blocking notifications for all major actions
- Four variants: success, error, warning, info
- Auto-dismiss with progress bar
- Bottom-right positioning with smooth animations
- Replaced old status messages throughout the app

Command Palette (Ctrl+P):
- Keyboard-driven quick access to all actions
- 14 built-in commands across 5 categories
- Real-time fuzzy search with arrow key navigation
- Shows keyboard shortcuts for each command
- Grouped by category with visual feedback

Auto-save & Recovery:
- Automatic draft saving (1s debounce)
- Per-character draft storage in localStorage
- Auto-recovery on app restart or character switch
- Draft age display (e.g., "2 hours ago")
- Auto-cleanup of drafts older than 7 days
- Clears draft when message is sent

Updated README with new Ctrl+P keyboard shortcut
Updated ROADMAP with Phase 8: Quality of Life & Polish section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 12:27:20 -07:00
50d3177e9e feat: implement chat branching and checkpoints
Add conversation branching system that allows creating and exploring alternate conversation paths from any message point. Each branch maintains its own complete message history.

Backend (Rust):
- Branch data structures with backward-compatible storage migration
- Tauri commands for create, switch, delete, rename, and list operations
- Automatic cleanup of child branches when parent is deleted

Frontend:
- Branch button on all messages for creating new branches
- Branch indicator badge in header showing active branch
- Branch manager modal with full branch list and controls
- Visual improvements to message action toolbar (more opaque, positioned above messages)

Branches are character-specific and persist across sessions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 08:17:08 -07:00
6 changed files with 1830 additions and 90 deletions

View File

@@ -62,6 +62,7 @@ Config stored in `~/.config/claudia/config.json`
- **Left/Right Arrow** - Navigate between response alternatives - **Left/Right Arrow** - Navigate between response alternatives
- **Escape** - Close panels/modals, cancel editing - **Escape** - Close panels/modals, cancel editing
- **Ctrl+K** - Focus message input - **Ctrl+K** - Focus message input
- **Ctrl+P** - Open command palette (quick access to all actions)
- **Ctrl+/** - Toggle Roleplay Tools panel - **Ctrl+/** - Toggle Roleplay Tools panel
## Roadmap ## Roadmap

View File

@@ -19,11 +19,12 @@
- Enhanced Message Controls (delete, pin, hide, continue, regenerate any message) - Enhanced Message Controls (delete, pin, hide, continue, regenerate any message)
- Token Counter (real-time display with per-section breakdown) - Token Counter (real-time display with per-section breakdown)
- Message Examples (character card examples injected into context) - Message Examples (character card examples injected into context)
- Chat Branching/Checkpoints (create, switch, delete, rename branches from any message)
### 🎯 Current Focus: Advanced Chat Management ### 🎯 Current Focus: Quality of Life & Polish
**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. **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) ## Phase 1: Core Roleplay Infrastructure (High Priority)
**Goal: Enable basic roleplay-focused prompt engineering** **Goal: Enable basic roleplay-focused prompt engineering**
@@ -87,13 +88,14 @@
## Phase 3: Advanced Chat Management (Medium Priority) ## Phase 3: Advanced Chat Management (Medium Priority)
**Goal: Non-linear conversation control** **Goal: Non-linear conversation control**
### 1. Chat Branching/Checkpoints ### 1. Chat Branching/Checkpoints
- [ ] Save conversation state at any message - [x] Save conversation state at any message
- [ ] Create branches from any point - [x] Create branches from any point
- [ ] Switch between branches - [x] Switch between branches
- [ ] Visual branch indicator in UI - [x] Visual branch indicator in UI
- [ ] Branch naming and organization - [x] Branch naming and organization
- [ ] Delete/merge branches - [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. **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. **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 ## Implementation Priority Ranking
### Must-Have for Basic Roleplay: ### Must-Have for Basic Roleplay:

View File

@@ -507,6 +507,47 @@ struct ChatHistory {
messages: Vec<Message>, messages: Vec<Message>,
} }
// New branching structures
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Branch {
id: String,
name: String,
created_at: i64, // Unix timestamp in milliseconds
#[serde(default)]
parent_branch_id: Option<String>,
#[serde(default)]
diverge_at_index: usize, // Message index in parent where this branch diverged
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct BranchedChatHistory {
#[serde(default = "default_branches")]
branches: Vec<Branch>,
#[serde(default = "default_active_branch")]
active_branch_id: String,
#[serde(default)]
branch_messages: std::collections::HashMap<String, Vec<Message>>,
}
fn default_branches() -> Vec<Branch> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
vec![Branch {
id: "main".to_string(),
name: "Main".to_string(),
created_at: timestamp,
parent_branch_id: None,
diverge_at_index: 0,
}]
}
fn default_active_branch() -> String {
"main".to_string()
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct ChatRequest { struct ChatRequest {
model: String, model: String,
@@ -951,21 +992,61 @@ fn save_config(config: &ApiConfig) -> Result<(), String> {
Ok(()) Ok(())
} }
fn load_history(character_id: &str) -> ChatHistory { // Load branched history (with backward compatibility)
fn load_branched_history(character_id: &str) -> BranchedChatHistory {
let path = get_character_history_path(character_id); let path = get_character_history_path(character_id);
if let Ok(contents) = fs::read_to_string(path) { if let Ok(contents) = fs::read_to_string(&path) {
let mut history: ChatHistory = serde_json::from_str(&contents).unwrap_or(ChatHistory { messages: vec![] }); // Try to load as new branched format first
if let Ok(mut branched) = serde_json::from_str::<BranchedChatHistory>(&contents) {
// Migrate old messages to new format // Migrate old messages to new format
for msg in &mut history.messages { for messages in branched.branch_messages.values_mut() {
for msg in messages {
msg.migrate(); msg.migrate();
} }
history }
} else { return branched;
ChatHistory { messages: vec![] } }
// Fall back to old linear format and migrate
if let Ok(mut old_history) = serde_json::from_str::<ChatHistory>(&contents) {
for msg in &mut old_history.messages {
msg.migrate();
}
// Convert to branched format
let mut branch_messages = HashMap::new();
branch_messages.insert("main".to_string(), old_history.messages);
return BranchedChatHistory {
branches: default_branches(),
active_branch_id: "main".to_string(),
branch_messages,
};
}
}
// Return empty history with main branch
let mut branch_messages = HashMap::new();
branch_messages.insert("main".to_string(), vec![]);
BranchedChatHistory {
branches: default_branches(),
active_branch_id: "main".to_string(),
branch_messages,
} }
} }
fn save_history(character_id: &str, history: &ChatHistory) -> Result<(), String> { // Legacy function - returns active branch messages
fn load_history(character_id: &str) -> ChatHistory {
let branched = load_branched_history(character_id);
let messages = branched.branch_messages
.get(&branched.active_branch_id)
.cloned()
.unwrap_or_default();
ChatHistory { messages }
}
fn save_branched_history(character_id: &str, history: &BranchedChatHistory) -> Result<(), String> {
let path = get_character_history_path(character_id); let path = get_character_history_path(character_id);
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?; fs::create_dir_all(parent).map_err(|e| e.to_string())?;
@@ -975,6 +1056,13 @@ fn save_history(character_id: &str, history: &ChatHistory) -> Result<(), String>
Ok(()) Ok(())
} }
// Legacy function - saves to active branch
fn save_history(character_id: &str, history: &ChatHistory) -> Result<(), String> {
let mut branched = load_branched_history(character_id);
branched.branch_messages.insert(branched.active_branch_id.clone(), history.messages.clone());
save_branched_history(character_id, &branched)
}
fn load_character(character_id: &str) -> Option<Character> { fn load_character(character_id: &str) -> Option<Character> {
let path = get_character_path(character_id); let path = get_character_path(character_id);
if let Ok(contents) = fs::read_to_string(path) { if let Ok(contents) = fs::read_to_string(path) {
@@ -3299,6 +3387,153 @@ async fn export_character_card(app_handle: tauri::AppHandle, character_id: Strin
Ok(output_path.to_string_lossy().to_string()) Ok(output_path.to_string_lossy().to_string())
} }
// ============================================================================
// Branch Management Commands
// ============================================================================
#[tauri::command]
fn create_branch(message_index: usize, branch_name: String) -> Result<Branch, String> {
let character = get_active_character();
let mut branched = load_branched_history(&character.id);
// Generate new branch ID
let branch_id = Uuid::new_v4().to_string();
// Get current branch messages
let current_messages = branched.branch_messages
.get(&branched.active_branch_id)
.ok_or_else(|| "Active branch not found".to_string())?;
// Validate message index
if message_index > current_messages.len() {
return Err(format!("Invalid message index: {}", message_index));
}
// Create new branch
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
let new_branch = Branch {
id: branch_id.clone(),
name: branch_name,
created_at: timestamp,
parent_branch_id: Some(branched.active_branch_id.clone()),
diverge_at_index: message_index,
};
// Copy messages up to divergence point
let branch_messages: Vec<Message> = current_messages[..message_index].to_vec();
// Add branch and its messages
branched.branches.push(new_branch.clone());
branched.branch_messages.insert(branch_id.clone(), branch_messages);
// Save and return
save_branched_history(&character.id, &branched)?;
Ok(new_branch)
}
#[tauri::command]
fn switch_branch(branch_id: String) -> Result<Vec<Message>, String> {
let character = get_active_character();
let mut branched = load_branched_history(&character.id);
// Verify branch exists
if !branched.branch_messages.contains_key(&branch_id) {
return Err(format!("Branch '{}' not found", branch_id));
}
// Switch active branch
branched.active_branch_id = branch_id.clone();
save_branched_history(&character.id, &branched)?;
// Return messages for the new active branch
let messages = branched.branch_messages
.get(&branch_id)
.cloned()
.unwrap_or_default();
Ok(messages)
}
#[tauri::command]
fn delete_branch(branch_id: String) -> Result<(), String> {
let character = get_active_character();
let mut branched = load_branched_history(&character.id);
// Cannot delete main branch
if branch_id == "main" {
return Err("Cannot delete main branch".to_string());
}
// Cannot delete active branch
if branch_id == branched.active_branch_id {
return Err("Cannot delete active branch. Switch to another branch first.".to_string());
}
// Remove branch
branched.branches.retain(|b| b.id != branch_id);
branched.branch_messages.remove(&branch_id);
// Also remove any child branches that depended on this one
let mut branches_to_remove = vec![];
for branch in &branched.branches {
if branch.parent_branch_id.as_ref() == Some(&branch_id) {
branches_to_remove.push(branch.id.clone());
}
}
for child_id in branches_to_remove {
branched.branches.retain(|b| b.id != child_id);
branched.branch_messages.remove(&child_id);
}
save_branched_history(&character.id, &branched)?;
Ok(())
}
#[tauri::command]
fn list_branches() -> Result<Vec<Branch>, String> {
let character = get_active_character();
let branched = load_branched_history(&character.id);
Ok(branched.branches)
}
#[tauri::command]
fn rename_branch(branch_id: String, new_name: String) -> Result<(), String> {
let character = get_active_character();
let mut branched = load_branched_history(&character.id);
// Find and rename the branch
let branch = branched.branches.iter_mut()
.find(|b| b.id == branch_id)
.ok_or_else(|| format!("Branch '{}' not found", branch_id))?;
branch.name = new_name;
save_branched_history(&character.id, &branched)?;
Ok(())
}
#[tauri::command]
fn get_active_branch_id() -> Result<String, String> {
let character = get_active_character();
let branched = load_branched_history(&character.id);
Ok(branched.active_branch_id)
}
#[tauri::command]
fn get_branch_info(branch_id: String) -> Result<Branch, String> {
let character = get_active_character();
let branched = load_branched_history(&character.id);
branched.branches.iter()
.find(|b| b.id == branch_id)
.cloned()
.ok_or_else(|| format!("Branch '{}' not found", branch_id))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
@@ -3362,7 +3597,14 @@ pub fn run() {
update_world_info_entry, update_world_info_entry,
delete_world_info_entry, delete_world_info_entry,
export_world_info, export_world_info,
import_world_info import_world_info,
create_branch,
switch_branch,
delete_branch,
list_branches,
rename_branch,
get_active_branch_id,
get_branch_info
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@@ -733,6 +733,33 @@
</footer> </footer>
</div> </div>
<!-- Toast Container -->
<div id="toast-container" class="toast-container"></div>
<!-- Command Palette -->
<div id="command-palette-modal" class="command-palette-modal" style="display: none;">
<div class="command-palette-overlay"></div>
<div class="command-palette-content">
<div class="command-palette-search">
<svg class="command-palette-search-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2"/>
<path d="M12.5 12.5L17 17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<input
type="text"
id="command-palette-input"
class="command-palette-input"
placeholder="Type a command or search..."
autocomplete="off"
/>
<kbd class="command-palette-hint">Esc to close</kbd>
</div>
<div id="command-palette-results" class="command-palette-results">
<!-- Command results will be dynamically populated here -->
</div>
</div>
</div>
<!-- Avatar zoom modal --> <!-- Avatar zoom modal -->
<div id="avatar-modal" class="avatar-modal" style="display: none;"> <div id="avatar-modal" class="avatar-modal" style="display: none;">
<div class="avatar-modal-overlay"></div> <div class="avatar-modal-overlay"></div>

File diff suppressed because it is too large Load Diff

View File

@@ -211,6 +211,151 @@ body {
color: var(--accent); color: var(--accent);
} }
/* Branch Badge */
.branch-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 12px;
font-size: 11px;
font-weight: 500;
color: #22c55e;
white-space: nowrap;
transition: all 0.2s ease;
}
.branch-badge:hover {
background: rgba(34, 197, 94, 0.15);
border-color: rgba(34, 197, 94, 0.5);
}
.branch-badge svg {
width: 12px;
height: 12px;
opacity: 0.8;
}
/* Branch Manager Modal */
.branch-manager-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10002;
display: flex;
align-items: center;
justify-content: center;
}
.branch-manager-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
cursor: pointer;
}
.branch-manager-content {
position: relative;
z-index: 1;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
width: 90%;
max-width: 600px;
max-height: 80vh;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
}
.branch-manager-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.branch-manager-header h3 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.branch-list {
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
max-height: 60vh;
}
.branch-item {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
}
.branch-item:hover {
border-color: var(--accent);
}
.branch-item.active {
background: rgba(99, 102, 241, 0.1);
border-color: var(--accent);
}
.branch-info {
flex: 1;
min-width: 0;
}
.branch-name {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 4px;
}
.branch-meta {
font-size: 11px;
color: var(--text-secondary);
}
.branch-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.branch-active-label {
font-size: 12px;
padding: 4px 12px;
background: var(--accent);
color: white;
border-radius: 6px;
font-weight: 500;
}
.icon-btn { .icon-btn {
width: 28px; width: 28px;
height: 28px; height: 28px;
@@ -371,19 +516,24 @@ body {
/* Message action buttons */ /* Message action buttons */
.message-actions { .message-actions {
position: absolute; position: absolute;
top: 8px; top: -4px;
display: flex; display: flex;
gap: 4px; gap: 4px;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
background: rgba(0, 0, 0, 0.6);
padding: 4px;
border-radius: 8px;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
} }
.message.user .message-actions { .message.user .message-actions {
right: 8px; right: 4px;
} }
.message.assistant .message-actions { .message.assistant .message-actions {
right: 8px; right: 4px;
} }
.message:hover .message-actions { .message:hover .message-actions {
@@ -391,26 +541,28 @@ body {
} }
.message-action-btn { .message-action-btn {
width: 24px; width: 28px;
height: 24px; height: 28px;
border: none; border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(12px);
border-radius: 6px; border-radius: 6px;
color: var(--text-secondary); color: rgba(255, 255, 255, 0.8);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: all 0.2s ease; transition: all 0.2s ease;
padding: 0; padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
} }
.message-action-btn:hover { .message-action-btn:hover {
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.95);
color: var(--text-primary); border-color: rgba(255, 255, 255, 0.3);
transform: scale(1.1); color: white;
transform: scale(1.05);
} }
.message-action-btn:active { .message-action-btn:active {
@@ -418,8 +570,8 @@ body {
} }
.message-action-btn svg { .message-action-btn svg {
width: 14px; width: 16px;
height: 14px; height: 16px;
} }
/* Enhanced message control buttons */ /* Enhanced message control buttons */
@@ -2029,3 +2181,435 @@ body.view-comfortable .message-content pre {
left: 0; 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;
}
}