Compare commits
2 Commits
86a9d54e70
...
10d95951a3
| Author | SHA1 | Date | |
|---|---|---|---|
| 10d95951a3 | |||
| 50d3177e9e |
@@ -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
|
||||
|
||||
167
ROADMAP.md
167
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:
|
||||
|
||||
@@ -507,6 +507,47 @@ struct ChatHistory {
|
||||
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)]
|
||||
struct ChatRequest {
|
||||
model: String,
|
||||
@@ -951,21 +992,61 @@ fn save_config(config: &ApiConfig) -> Result<(), String> {
|
||||
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);
|
||||
if let Ok(contents) = fs::read_to_string(path) {
|
||||
let mut history: ChatHistory = serde_json::from_str(&contents).unwrap_or(ChatHistory { messages: vec![] });
|
||||
// Migrate old messages to new format
|
||||
for msg in &mut history.messages {
|
||||
msg.migrate();
|
||||
if let Ok(contents) = fs::read_to_string(&path) {
|
||||
// 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
|
||||
for messages in branched.branch_messages.values_mut() {
|
||||
for msg in messages {
|
||||
msg.migrate();
|
||||
}
|
||||
}
|
||||
return branched;
|
||||
}
|
||||
history
|
||||
} else {
|
||||
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);
|
||||
if let Some(parent) = path.parent() {
|
||||
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(())
|
||||
}
|
||||
|
||||
// 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> {
|
||||
let path = get_character_path(character_id);
|
||||
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())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -3362,7 +3597,14 @@ pub fn run() {
|
||||
update_world_info_entry,
|
||||
delete_world_info_entry,
|
||||
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!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -733,6 +733,33 @@
|
||||
</footer>
|
||||
</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 -->
|
||||
<div id="avatar-modal" class="avatar-modal" style="display: none;">
|
||||
<div class="avatar-modal-overlay"></div>
|
||||
|
||||
847
src/main.js
847
src/main.js
File diff suppressed because it is too large
Load Diff
614
src/styles.css
614
src/styles.css
@@ -211,6 +211,151 @@ body {
|
||||
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 {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@@ -371,19 +516,24 @@ body {
|
||||
/* Message action buttons */
|
||||
.message-actions {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: -4px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
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 {
|
||||
right: 8px;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.message.assistant .message-actions {
|
||||
right: 8px;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.message:hover .message-actions {
|
||||
@@ -391,26 +541,28 @@ body {
|
||||
}
|
||||
|
||||
.message-action-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.message-action-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: var(--text-primary);
|
||||
transform: scale(1.1);
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.message-action-btn:active {
|
||||
@@ -418,8 +570,8 @@ body {
|
||||
}
|
||||
|
||||
.message-action-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Enhanced message control buttons */
|
||||
@@ -2029,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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user