Compare commits
10 Commits
0712b1c422
...
84d3e0df67
| Author | SHA1 | Date | |
|---|---|---|---|
| 84d3e0df67 | |||
| 30e6af61ca | |||
| 32fc57fab0 | |||
| efa3ccbd26 | |||
| b9ea771ff0 | |||
| 4866c11245 | |||
| e0239aceda | |||
| 4694114ff9 | |||
| ab6ae14bbc | |||
| a9b686f0d1 |
71
README.md
71
README.md
@@ -1,28 +1,37 @@
|
|||||||
# Claudia
|
# Claudia
|
||||||
|
|
||||||
Beautiful AI desktop companion built with Tauri and Rust.
|
Beautiful AI roleplay desktop companion built with Tauri and Rust.
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
|
||||||
|
Claudia aims to be a lightweight, desktop-native alternative to SillyTavern, focusing on roleplay and character-based interactions while maintaining a clean, modern interface.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Core Features
|
### Core Chat Features
|
||||||
- 🎨 **Beautiful glassmorphic UI** with gradient backgrounds and blur effects
|
- 🎨 **Beautiful glassmorphic UI** - Modern design with gradient backgrounds
|
||||||
- 🔧 **Bring-your-own-API** - supports any Anthropic-compatible API
|
- 🔧 **Bring-your-own-API** - Supports any Anthropic-compatible API
|
||||||
- ✅ **API validation** via /v1/models endpoint
|
- ✅ **API validation** - Automatic model detection via /v1/models
|
||||||
- 💬 **Full conversation context** - AI remembers your entire conversation
|
- 💬 **Full conversation context** - AI remembers your entire conversation
|
||||||
- 💾 **Persistent chat history** - conversations saved between sessions
|
- 💾 **Persistent chat history** - Conversations saved per character
|
||||||
- 🎯 **Custom window controls** - drag, minimize, maximize, close
|
- 🎯 **Streaming responses** - Real-time token display (optional)
|
||||||
|
|
||||||
|
### Character System
|
||||||
|
- 🎭 **Multiple characters** - Switch between different AI personas
|
||||||
|
- 🖼️ **Character avatars** - Upload custom images with zoom preview
|
||||||
|
- 📇 **V2/V3 character cards** - Import/export Tavern-compatible cards
|
||||||
|
- ✏️ **Full character editor** - All v2/v3 fields supported (description, scenario, examples, etc.)
|
||||||
|
|
||||||
|
### Advanced Chat Features
|
||||||
|
- 🔄 **Message swipes** - Generate multiple responses and swipe between them
|
||||||
|
- ✏️ **Message editing** - Edit messages and regenerate from any point
|
||||||
|
- 🔀 **Chat branching** - Explore alternate conversation paths
|
||||||
|
|
||||||
### Message Display
|
### Message Display
|
||||||
- 📝 **Full markdown rendering** - headers, lists, tables, links, blockquotes
|
- 📝 **Full markdown rendering** - Headers, lists, tables, links, blockquotes
|
||||||
- 🎨 **Syntax highlighting** - beautiful code blocks with highlight.js
|
- 🎨 **Syntax highlighting** - Beautiful code blocks with highlight.js
|
||||||
- 📋 **Copy code blocks** - one-click copy button on hover
|
- 📋 **Copy code blocks** - One-click copy button on hover
|
||||||
- ✨ **Smooth animations** - elegant message transitions
|
- ✨ **Smooth animations** - Elegant message transitions
|
||||||
|
|
||||||
### User Experience
|
|
||||||
- ⌨️ **Keyboard shortcuts** - Enter or Ctrl+Enter to send, Shift+Enter for new lines
|
|
||||||
- 🗑️ **Clear conversations** - easily start fresh
|
|
||||||
- 🎯 **Auto-resizing input** - textarea grows with your message
|
|
||||||
- 🎭 **Light/dark mode** - automatic based on system preferences
|
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
@@ -53,9 +62,37 @@ On first launch, click settings and configure:
|
|||||||
### Keyboard Shortcuts
|
### Keyboard Shortcuts
|
||||||
- **Enter** - Send message
|
- **Enter** - Send message
|
||||||
- **Shift+Enter** - New line in message
|
- **Shift+Enter** - New line in message
|
||||||
|
- **Up Arrow** - Edit last user message
|
||||||
|
- **Left/Right Arrow** - Swipe between alternative responses
|
||||||
|
|
||||||
|
### Character Management
|
||||||
|
- **Character Dropdown** - Switch between characters
|
||||||
|
- **Settings → Character Tab** - Edit current character
|
||||||
|
- **Import v2 Card** - Import Tavern character cards (PNG format)
|
||||||
|
- **Export v2 Card** - Export character as Tavern-compatible card
|
||||||
|
|
||||||
### Interface
|
### Interface
|
||||||
- **Drag header** - Move window around your desktop
|
- **Drag header** - Move window around your desktop
|
||||||
- **Trash icon** - Clear conversation history
|
- **Trash icon** - Clear conversation history
|
||||||
- **Settings icon** - Configure API settings
|
- **Settings icon** - Configure API settings
|
||||||
- **Minimize/Maximize** - Window controls
|
- **Minimize/Maximize** - Window controls
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
Claudia is being developed to become a full-featured roleplay platform comparable to SillyTavern. See [ROADMAP.md](ROADMAP.md) for detailed plans including:
|
||||||
|
|
||||||
|
**Coming Soon:**
|
||||||
|
- 📚 World Info/Lorebooks for dynamic context
|
||||||
|
- 📝 Author's Note for better prompt control
|
||||||
|
- 👤 User Personas for identity management
|
||||||
|
- 😊 Character Expression Sprites
|
||||||
|
- 🔢 Token Counter and context visualization
|
||||||
|
- 👥 Group Chats with multiple characters
|
||||||
|
- ⚡ Quick Replies and macro system
|
||||||
|
|
||||||
|
**Current Version:** v0.1.0 - Basic character chat with swipes and card import/export
|
||||||
|
**Next Version:** v0.2.0 - Roleplay Foundation (World Info, Author's Note, Token Counter)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
This is a personal project, but feedback and suggestions are welcome! If you encounter bugs or have feature requests, please open an issue on GitHub.
|
||||||
|
|||||||
347
ROADMAP.md
Normal file
347
ROADMAP.md
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
# Claudia Roleplay Enhancement Roadmap
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
### ✅ Implemented Features
|
||||||
|
- V2/V3 Character Card Import/Export
|
||||||
|
- Message Swipes (multiple response alternatives)
|
||||||
|
- Streaming Responses with toggle
|
||||||
|
- Character Management (multiple characters)
|
||||||
|
- Character Avatars with upload and zoom
|
||||||
|
- Expanded Character Editor (all v2/v3 fields)
|
||||||
|
|
||||||
|
### 🎯 Current Focus: UI/UX Improvements
|
||||||
|
**Decision:** Before adding complex roleplay features, we're focusing on polishing the existing UI/UX to establish a solid foundation. This includes better visual design, improved workflows, and enhanced user experience.
|
||||||
|
|
||||||
|
See "Phase 7: Polish & UX" section for details on UI improvements being prioritized.
|
||||||
|
|
||||||
|
## Phase 1: Core Roleplay Infrastructure (High Priority)
|
||||||
|
**Goal: Enable basic roleplay-focused prompt engineering**
|
||||||
|
|
||||||
|
### 1. World Info/Lorebook System
|
||||||
|
- [ ] Create UI for managing lorebook entries (keyword, content, priority)
|
||||||
|
- [ ] Implement keyword detection in recent messages
|
||||||
|
- [ ] Add context injection before message generation
|
||||||
|
- [ ] Support recursive entry activation
|
||||||
|
- [ ] Per-character lorebook assignment
|
||||||
|
- [ ] Import/export lorebook files
|
||||||
|
|
||||||
|
**Why Important:** World Info is the foundation of consistent roleplay. It allows dynamic context injection based on what's currently relevant in the conversation, saving tokens while maintaining world consistency.
|
||||||
|
|
||||||
|
### 2. Author's Note
|
||||||
|
- [ ] Add configurable Author's Note field (inserted at depth 1-5)
|
||||||
|
- [ ] Position control (after system, before/after examples, etc.)
|
||||||
|
- [ ] Per-character Author's Note support
|
||||||
|
- [ ] Template variables in Author's Note
|
||||||
|
|
||||||
|
**Why Important:** Author's Note is considered better than system prompts for roleplay because it appears closer to the actual conversation, reducing AI tendency to ignore or forget instructions.
|
||||||
|
|
||||||
|
### 3. Jailbreak Templates
|
||||||
|
- [ ] Add jailbreak template field in settings
|
||||||
|
- [ ] Preset jailbreak templates for roleplay
|
||||||
|
- [ ] Per-character jailbreak override option
|
||||||
|
- [ ] Template preview and testing
|
||||||
|
|
||||||
|
**Why Important:** Many roleplay scenarios require specific prompting to work well with API safety filters and to maintain character consistency.
|
||||||
|
|
||||||
|
## Phase 2: Enhanced Character Features (High Priority)
|
||||||
|
**Goal: Better character representation and user identity**
|
||||||
|
|
||||||
|
### 1. User Personas
|
||||||
|
- [ ] Create persona management UI (name, description, avatar)
|
||||||
|
- [ ] Chat-level persona locking
|
||||||
|
- [ ] Character-level persona locking
|
||||||
|
- [ ] Default persona setting
|
||||||
|
- [ ] Quick persona switching
|
||||||
|
|
||||||
|
**Why Important:** Allows users to have multiple identities for different roleplay scenarios without manually changing their name and description each time.
|
||||||
|
|
||||||
|
### 2. Character Expressions/Sprites
|
||||||
|
- [ ] Support for emotion-based character images
|
||||||
|
- [ ] Sentiment analysis of AI responses (local model)
|
||||||
|
- [ ] 28+ emotion presets (happy, sad, angry, neutral, etc.)
|
||||||
|
- [ ] Expression sprite packs (import/export)
|
||||||
|
- [ ] Manual expression override with /emote command
|
||||||
|
- [ ] Sprite positioning options (beside chat, behind chat, etc.)
|
||||||
|
|
||||||
|
**Why Important:** Visual representation of character emotions dramatically enhances immersion and makes conversations feel more alive.
|
||||||
|
|
||||||
|
### 3. Message Examples in Context
|
||||||
|
- [ ] Actually use mes_example field from character cards
|
||||||
|
- [ ] Format and inject into prompt properly
|
||||||
|
- [ ] Position control in context
|
||||||
|
- [ ] Token budget allocation for examples
|
||||||
|
|
||||||
|
**Why Important:** Message examples help the AI understand the character's voice and writing style, leading to more accurate portrayals.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
**Why Important:** Roleplay often involves exploring "what if" scenarios. Branching lets you explore different conversation paths without losing previous progress.
|
||||||
|
|
||||||
|
### 2. Enhanced Message Controls
|
||||||
|
- [ ] Delete individual messages (not just clearing all)
|
||||||
|
- [ ] Regenerate any message (not just last)
|
||||||
|
- [ ] Continue incomplete messages
|
||||||
|
- [ ] Message pinning (keep certain messages in context)
|
||||||
|
- [ ] Message folding/hiding
|
||||||
|
- [ ] Bulk message operations
|
||||||
|
|
||||||
|
**Why Important:** Fine-grained control over conversation history allows users to craft the perfect roleplay session.
|
||||||
|
|
||||||
|
### 3. Timeline Visualization
|
||||||
|
- [ ] Visual tree of chat branches
|
||||||
|
- [ ] Quick navigation between branches
|
||||||
|
- [ ] Branch metadata (creation date, message count, etc.)
|
||||||
|
- [ ] Visual diff between branches
|
||||||
|
- [ ] Merge branch capability
|
||||||
|
|
||||||
|
**Why Important:** Makes managing complex branching conversations intuitive and prevents users from getting lost.
|
||||||
|
|
||||||
|
## Phase 4: Multi-Character/Group Chats (Medium Priority)
|
||||||
|
**Goal: Enable complex multi-character scenarios**
|
||||||
|
|
||||||
|
### 1. Group Chat Foundation
|
||||||
|
- [ ] Create group chat data structure
|
||||||
|
- [ ] UI for managing group members
|
||||||
|
- [ ] Add/remove characters from groups
|
||||||
|
- [ ] Group chat history management
|
||||||
|
- [ ] Per-group settings
|
||||||
|
|
||||||
|
**Why Important:** Many roleplay scenarios involve multiple characters interacting. Group chats enable DM-style gameplay and complex social scenarios.
|
||||||
|
|
||||||
|
### 2. Reply Management
|
||||||
|
- [ ] Manual character selection
|
||||||
|
- [ ] Natural order (mention-based)
|
||||||
|
- [ ] Talkativeness settings per character (0-100%)
|
||||||
|
- [ ] Auto-mode (characters respond automatically)
|
||||||
|
- [ ] Character muting/unmuting
|
||||||
|
- [ ] Reply order presets
|
||||||
|
|
||||||
|
**Why Important:** Gives users control over conversation flow while allowing for spontaneous multi-character interactions.
|
||||||
|
|
||||||
|
### 3. Group Chat UI
|
||||||
|
- [ ] Character indicators on messages
|
||||||
|
- [ ] Character list sidebar
|
||||||
|
- [ ] Mute/unmute controls
|
||||||
|
- [ ] Character ordering/priority
|
||||||
|
- [ ] Group-wide lorebook support
|
||||||
|
|
||||||
|
**Why Important:** Clear visual indicators make group conversations easy to follow.
|
||||||
|
|
||||||
|
## Phase 5: Context & Token Management (Medium Priority)
|
||||||
|
**Goal: Visibility and control over context usage**
|
||||||
|
|
||||||
|
### 1. Token Counter
|
||||||
|
- [ ] Real-time token count display
|
||||||
|
- [ ] Per-section breakdown (system, history, WI, etc.)
|
||||||
|
- [ ] Visual context budget indicator
|
||||||
|
- [ ] Dotted line showing context cutoff in chat
|
||||||
|
- [ ] Warning when approaching limit
|
||||||
|
|
||||||
|
**Why Important:** Understanding what's in context and what's being cut is crucial for debugging issues and optimizing prompts.
|
||||||
|
|
||||||
|
### 2. Context Templates
|
||||||
|
- [ ] Customizable prompt assembly order
|
||||||
|
- [ ] Handlebars template support
|
||||||
|
- [ ] Presets for different model types (Alpaca, ChatML, Llama, etc.)
|
||||||
|
- [ ] Template preview
|
||||||
|
- [ ] Variable substitution visualization
|
||||||
|
|
||||||
|
**Why Important:** Different models expect different prompt formats. Templates ensure prompts are formatted correctly for each model.
|
||||||
|
|
||||||
|
### 3. Smart Context Management
|
||||||
|
- [ ] Summarization of old messages
|
||||||
|
- [ ] Automatic message trimming
|
||||||
|
- [ ] Priority-based context allocation
|
||||||
|
- [ ] Context budget per source (system, WI, history, etc.)
|
||||||
|
- [ ] Smart message selection (keep important messages)
|
||||||
|
|
||||||
|
**Why Important:** Efficient context usage means longer, more coherent conversations without running out of tokens.
|
||||||
|
|
||||||
|
## Phase 6: Power User Features (Low Priority)
|
||||||
|
**Goal: Advanced customization and automation**
|
||||||
|
|
||||||
|
### 1. Quick Replies
|
||||||
|
- [ ] Preset message buttons
|
||||||
|
- [ ] Macro support in quick replies
|
||||||
|
- [ ] Import/export QR sets
|
||||||
|
- [ ] Character-specific QR sets
|
||||||
|
- [ ] Conditional quick replies
|
||||||
|
- [ ] Quick reply categories/folders
|
||||||
|
|
||||||
|
**Why Important:** Speeds up common actions and reduces repetitive typing in roleplay scenarios.
|
||||||
|
|
||||||
|
### 2. Macro System
|
||||||
|
- [ ] Basic macros ({{user}}, {{char}}, {{random}}, etc.)
|
||||||
|
- [ ] Date/time macros
|
||||||
|
- [ ] Conditional macros
|
||||||
|
- [ ] Custom macro definitions
|
||||||
|
- [ ] Nested macro support
|
||||||
|
- [ ] Macro debugging
|
||||||
|
|
||||||
|
**Why Important:** Makes prompts and messages dynamic and reusable across different scenarios.
|
||||||
|
|
||||||
|
### 3. Regex Scripts
|
||||||
|
- [ ] Global and character-scoped scripts
|
||||||
|
- [ ] Text transformation on messages
|
||||||
|
- [ ] Auto-markdown formatting
|
||||||
|
- [ ] Import/export regex presets
|
||||||
|
- [ ] Regex testing interface
|
||||||
|
- [ ] Script priority/ordering
|
||||||
|
|
||||||
|
**Why Important:** Allows automatic text formatting, correction, and enhancement without manual intervention.
|
||||||
|
|
||||||
|
### 4. Hotkey System
|
||||||
|
- [ ] Customizable keyboard shortcuts
|
||||||
|
- [ ] Quick actions (regen, edit, delete, etc.)
|
||||||
|
- [ ] Markdown formatting hotkeys
|
||||||
|
- [ ] Quick Reply hotkeys
|
||||||
|
- [ ] Navigation hotkeys
|
||||||
|
- [ ] Hotkey conflict detection
|
||||||
|
|
||||||
|
**Why Important:** Power users rely on keyboard shortcuts for efficient workflow.
|
||||||
|
|
||||||
|
## Phase 7: Polish & UX (Ongoing)
|
||||||
|
**Goal: Better user experience for roleplay**
|
||||||
|
|
||||||
|
### 1. Instruct Mode Support
|
||||||
|
- [ ] Preset templates (Alpaca, ChatML, Llama, etc.)
|
||||||
|
- [ ] Custom template creation
|
||||||
|
- [ ] Auto-detect model format from API
|
||||||
|
- [ ] Instruction wrapping for system/user/assistant messages
|
||||||
|
|
||||||
|
**Why Important:** Ensures compatibility with instruction-tuned models that expect specific formats.
|
||||||
|
|
||||||
|
### 2. Export/Import Improvements
|
||||||
|
- [ ] Export chats as markdown
|
||||||
|
- [ ] Export chats as formatted text
|
||||||
|
- [ ] Export chats as JSON with metadata
|
||||||
|
- [ ] Import chats from other formats
|
||||||
|
- [ ] Bulk character import
|
||||||
|
- [ ] Character pack support (multiple characters + lorebooks)
|
||||||
|
|
||||||
|
**Why Important:** Sharing and migrating content between platforms and backing up work.
|
||||||
|
|
||||||
|
### 3. UI Enhancements
|
||||||
|
- [ ] Message timestamps
|
||||||
|
- [ ] Character indicators in messages
|
||||||
|
- [ ] Better settings organization (categories, search)
|
||||||
|
- [ ] Theme customization (colors, fonts, etc.)
|
||||||
|
- [ ] Compact/cozy view modes
|
||||||
|
- [ ] Responsive design for different screen sizes
|
||||||
|
- [ ] Accessibility improvements
|
||||||
|
|
||||||
|
**Why Important:** Better UI means less friction and more immersion in roleplay.
|
||||||
|
|
||||||
|
## Implementation Priority Ranking
|
||||||
|
|
||||||
|
### Must-Have for Basic Roleplay:
|
||||||
|
1. **World Info/Lorebooks** - Core feature for consistent roleplay
|
||||||
|
2. **Author's Note** - Better prompt control than system prompts alone
|
||||||
|
3. **Token Counter** - Visibility into what's happening
|
||||||
|
4. **Message Examples Usage** - Better character accuracy
|
||||||
|
|
||||||
|
### Important for Good Roleplay:
|
||||||
|
5. **User Personas** - Identity management
|
||||||
|
6. **Chat Branching** - Non-linear exploration
|
||||||
|
7. **Enhanced Message Controls** - Fine-grained editing
|
||||||
|
8. **Jailbreak Templates** - Handle various scenarios
|
||||||
|
|
||||||
|
### Great for Enhanced Experience:
|
||||||
|
9. **Expression Sprites** - Visual immersion
|
||||||
|
10. **Quick Replies + Macros** - Efficiency
|
||||||
|
11. **Context Templates** - Model compatibility
|
||||||
|
12. **Group Chats** - Complex scenarios
|
||||||
|
|
||||||
|
### Nice to Have:
|
||||||
|
13. **Timeline Visualization** - Advanced branch management
|
||||||
|
14. **Regex Scripts** - Automation
|
||||||
|
15. **Hotkeys** - Power user efficiency
|
||||||
|
16. **Smart Context Management** - Optimization
|
||||||
|
|
||||||
|
## Research Sources
|
||||||
|
|
||||||
|
This roadmap is based on research into SillyTavern's features and best practices from the roleplay AI community:
|
||||||
|
- SillyTavern official documentation (docs.sillytavern.app)
|
||||||
|
- Character card specifications (V2/V3 format)
|
||||||
|
- Community presets and guides on HuggingFace
|
||||||
|
- Roleplay community feedback and feature requests
|
||||||
|
|
||||||
|
## Technical Considerations
|
||||||
|
|
||||||
|
### Data Structures Needed:
|
||||||
|
- Lorebook entries (keyword, content, priority, insertion order, depth)
|
||||||
|
- Personas (name, description, avatar, chat/character locks)
|
||||||
|
- Chat branches (branch point, parent branch, metadata)
|
||||||
|
- Expression mappings (emotion → image file)
|
||||||
|
- Quick replies (text, macros, conditions, categories)
|
||||||
|
- Context templates (format strings, variables, presets)
|
||||||
|
|
||||||
|
### Backend Changes Required:
|
||||||
|
- Context assembly refactor (modular system for injecting different sources)
|
||||||
|
- Token counting integration (model-specific tokenizers)
|
||||||
|
- Sentiment analysis (local model or API integration)
|
||||||
|
- Branching chat storage (tree structure instead of linear)
|
||||||
|
- Group chat message routing (multi-character generation)
|
||||||
|
|
||||||
|
### UI Additions Needed:
|
||||||
|
- Lorebook editor panel
|
||||||
|
- Persona management panel
|
||||||
|
- Branch visualization widget
|
||||||
|
- Token counter display
|
||||||
|
- Group chat member list
|
||||||
|
- Quick reply buttons
|
||||||
|
- Expression sprite overlay
|
||||||
|
- Context template editor
|
||||||
|
|
||||||
|
## Version Milestones
|
||||||
|
|
||||||
|
### v0.2.0 - "Roleplay Foundation"
|
||||||
|
- World Info/Lorebooks
|
||||||
|
- Author's Note
|
||||||
|
- Token Counter
|
||||||
|
- Better Message Controls
|
||||||
|
|
||||||
|
### v0.3.0 - "Character Enhancement"
|
||||||
|
- User Personas
|
||||||
|
- Expression Sprites
|
||||||
|
- Message Examples Usage
|
||||||
|
- Jailbreak Templates
|
||||||
|
|
||||||
|
### v0.4.0 - "Advanced Chat"
|
||||||
|
- Chat Branching
|
||||||
|
- Timeline Visualization
|
||||||
|
- Group Chats (basic)
|
||||||
|
|
||||||
|
### v0.5.0 - "Power User"
|
||||||
|
- Quick Replies
|
||||||
|
- Macros
|
||||||
|
- Regex Scripts
|
||||||
|
- Hotkeys
|
||||||
|
|
||||||
|
### v1.0.0 - "Feature Complete"
|
||||||
|
- All planned features implemented
|
||||||
|
- Polished UI
|
||||||
|
- Comprehensive documentation
|
||||||
|
- Import/Export from SillyTavern
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Focus on **compatibility with SillyTavern** where possible (character cards, lorebooks, etc.)
|
||||||
|
- Keep **performance** in mind - roleplay sessions can be long
|
||||||
|
- Maintain **desktop-first** design - power users prefer desktop interfaces
|
||||||
|
- Consider **offline-first** approach - local models are popular for roleplay
|
||||||
|
- Remember **privacy** - roleplay content is often sensitive
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Last updated: 2025-10-14
|
||||||
@@ -174,24 +174,38 @@ struct Message {
|
|||||||
swipes: Vec<String>,
|
swipes: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
current_swipe: usize,
|
current_swipe: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
timestamp: i64, // Unix timestamp in milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
fn new_user(content: String) -> Self {
|
fn new_user(content: String) -> Self {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
role: "user".to_string(),
|
role: "user".to_string(),
|
||||||
content: content.clone(),
|
content: content.clone(),
|
||||||
swipes: vec![content],
|
swipes: vec![content],
|
||||||
current_swipe: 0,
|
current_swipe: 0,
|
||||||
|
timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_assistant(content: String) -> Self {
|
fn new_assistant(content: String) -> Self {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
content: content.clone(),
|
content: content.clone(),
|
||||||
swipes: vec![content],
|
swipes: vec![content],
|
||||||
current_swipe: 0,
|
current_swipe: 0,
|
||||||
|
timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
151
src/index.html
151
src/index.html
@@ -55,7 +55,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="settings-panel" id="settings-panel" style="display: none;">
|
<!-- Settings overlay backdrop -->
|
||||||
|
<div class="settings-overlay" id="settings-overlay"></div>
|
||||||
|
|
||||||
|
<div class="settings-panel" id="settings-panel">
|
||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
<button id="close-settings-btn" class="icon-btn">
|
<button id="close-settings-btn" class="icon-btn">
|
||||||
@@ -69,6 +72,7 @@
|
|||||||
<div class="settings-tabs">
|
<div class="settings-tabs">
|
||||||
<button class="tab-btn active" data-tab="api">API</button>
|
<button class="tab-btn active" data-tab="api">API</button>
|
||||||
<button class="tab-btn" data-tab="character">Character</button>
|
<button class="tab-btn" data-tab="character">Character</button>
|
||||||
|
<button class="tab-btn" data-tab="appearance">Appearance</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="api-tab" class="tab-content active">
|
<div id="api-tab" class="tab-content active">
|
||||||
@@ -125,6 +129,18 @@
|
|||||||
<label for="character-settings-select">Select Character</label>
|
<label for="character-settings-select">Select Character</label>
|
||||||
<select id="character-settings-select"></select>
|
<select id="character-settings-select"></select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Basic Info Section -->
|
||||||
|
<div class="settings-section" data-section="basic">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div class="settings-section-title">
|
||||||
|
<svg class="settings-section-icon" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Basic Information
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-name">Character Name</label>
|
<label for="character-name">Character Name</label>
|
||||||
<input
|
<input
|
||||||
@@ -174,9 +190,22 @@
|
|||||||
rows="2"
|
rows="2"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Roleplay Details Section -->
|
||||||
|
<div class="settings-section collapsed" data-section="roleplay">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div class="settings-section-title">
|
||||||
|
<svg class="settings-section-icon" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Roleplay Details
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-personality">Personality Tags (Optional)</label>
|
<label for="character-personality">Personality Tags</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="character-personality"
|
id="character-personality"
|
||||||
@@ -185,16 +214,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-description">Description (Optional)</label>
|
<label for="character-description">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="character-description"
|
id="character-description"
|
||||||
placeholder="Detailed character description, appearance, background..."
|
placeholder="Detailed character description, appearance, background..."
|
||||||
rows="10"
|
rows="8"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-scenario">Scenario (Optional)</label>
|
<label for="character-scenario">Scenario</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="character-scenario"
|
id="character-scenario"
|
||||||
placeholder="The setting or situation where the character exists..."
|
placeholder="The setting or situation where the character exists..."
|
||||||
@@ -203,16 +232,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-mes-example">Message Example (Optional)</label>
|
<label for="character-mes-example">Message Examples</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="character-mes-example"
|
id="character-mes-example"
|
||||||
placeholder="Example dialogue from the character..."
|
placeholder="Example dialogue from the character..."
|
||||||
rows="4"
|
rows="4"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Settings Section -->
|
||||||
|
<div class="settings-section collapsed" data-section="advanced">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div class="settings-section-title">
|
||||||
|
<svg class="settings-section-icon" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Advanced Settings
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-post-history">Post-History Instructions (Optional)</label>
|
<label for="character-post-history">Post-History Instructions</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="character-post-history"
|
id="character-post-history"
|
||||||
placeholder="Instructions to apply after chat history..."
|
placeholder="Instructions to apply after chat history..."
|
||||||
@@ -221,16 +263,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-alt-greetings">Alternate Greetings (Optional)</label>
|
<label for="character-alt-greetings">Alternate Greetings</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="character-alt-greetings"
|
id="character-alt-greetings"
|
||||||
placeholder="One greeting per line..."
|
placeholder="One greeting per line..."
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Metadata Section -->
|
||||||
|
<div class="settings-section collapsed" data-section="metadata">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<div class="settings-section-title">
|
||||||
|
<svg class="settings-section-icon" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Metadata
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section-content">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-tags">Tags (Optional)</label>
|
<label for="character-tags">Tags</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="character-tags"
|
id="character-tags"
|
||||||
@@ -239,7 +294,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-creator">Creator (Optional)</label>
|
<label for="character-creator">Creator</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="character-creator"
|
id="character-creator"
|
||||||
@@ -248,7 +303,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-version">Character Version (Optional)</label>
|
<label for="character-version">Character Version</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="character-version"
|
id="character-version"
|
||||||
@@ -257,26 +312,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="character-creator-notes">Creator Notes (Optional)</label>
|
<label for="character-creator-notes">Creator Notes</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="character-creator-notes"
|
id="character-creator-notes"
|
||||||
placeholder="Notes from the creator..."
|
placeholder="Notes from the creator..."
|
||||||
rows="2"
|
rows="2"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="character-message" class="validation-message"></div>
|
<div id="character-message" class="validation-message"></div>
|
||||||
|
|
||||||
<button type="submit" id="save-character-btn" class="btn-primary">
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<button type="submit" id="save-character-btn" class="btn-primary" style="flex: 1;">
|
||||||
Save Character
|
Save Character
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="delete-character-btn" class="btn-danger">
|
<button type="button" id="delete-character-btn" class="btn-danger">
|
||||||
Delete Character
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border-color);">
|
<div style="display: flex; gap: 8px;">
|
||||||
<label>Character Card Import/Export</label>
|
|
||||||
<div style="display: flex; gap: 0.5rem;">
|
|
||||||
<button type="button" id="import-character-btn" class="btn-secondary" style="flex: 1;">
|
<button type="button" id="import-character-btn" class="btn-secondary" style="flex: 1;">
|
||||||
Import v2 Card
|
Import v2 Card
|
||||||
</button>
|
</button>
|
||||||
@@ -284,9 +341,67 @@
|
|||||||
Export v2 Card
|
Export v2 Card
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="appearance-tab" class="tab-content">
|
||||||
|
<div class="settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="theme-select">Theme</label>
|
||||||
|
<select id="theme-select" class="theme-select">
|
||||||
|
<option value="dark">Dark (Default)</option>
|
||||||
|
<option value="darker">Darker</option>
|
||||||
|
<option value="midnight">Midnight Blue</option>
|
||||||
|
<option value="forest">Forest</option>
|
||||||
|
<option value="sunset">Sunset</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
</select>
|
||||||
|
<small style="color: var(--text-secondary); margin-top: 4px; display: block;">Choose your preferred color scheme</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="view-mode-select">View Mode</label>
|
||||||
|
<select id="view-mode-select" class="view-mode-select">
|
||||||
|
<option value="compact">Compact - Tight spacing, smaller text</option>
|
||||||
|
<option value="cozy">Cozy - Balanced spacing (Default)</option>
|
||||||
|
<option value="comfortable">Comfortable - Spacious layout</option>
|
||||||
|
</select>
|
||||||
|
<small style="color: var(--text-secondary); margin-top: 4px; display: block;">Adjust message density and spacing</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="font-size-slider">
|
||||||
|
Font Size: <span id="font-size-value">100%</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="font-size-slider"
|
||||||
|
min="80"
|
||||||
|
max="140"
|
||||||
|
value="100"
|
||||||
|
step="10"
|
||||||
|
class="font-size-slider"
|
||||||
|
/>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-top: 4px;">
|
||||||
|
<small style="color: var(--text-secondary);">Small (80%)</small>
|
||||||
|
<small style="color: var(--text-secondary);">Large (140%)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="theme-preview-container">
|
||||||
|
<div class="theme-preview-label">Preview</div>
|
||||||
|
<div class="theme-preview">
|
||||||
|
<div class="theme-preview-message user-preview">
|
||||||
|
<div class="theme-preview-content">User message</div>
|
||||||
|
</div>
|
||||||
|
<div class="theme-preview-message assistant-preview">
|
||||||
|
<div class="theme-preview-avatar"></div>
|
||||||
|
<div class="theme-preview-content">Assistant response</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="input-container">
|
<footer class="input-container">
|
||||||
|
|||||||
522
src/main.js
522
src/main.js
@@ -14,6 +14,193 @@ let newCharacterBtn;
|
|||||||
let currentCharacter = null;
|
let currentCharacter = null;
|
||||||
let pendingAvatarPath = null;
|
let pendingAvatarPath = null;
|
||||||
|
|
||||||
|
// Theme definitions
|
||||||
|
const themes = {
|
||||||
|
dark: {
|
||||||
|
name: 'Dark (Default)',
|
||||||
|
bgPrimary: '#1a1a1a',
|
||||||
|
bgSecondary: '#252525',
|
||||||
|
bgTertiary: '#2f2f2f',
|
||||||
|
textPrimary: '#e8e8e8',
|
||||||
|
textSecondary: '#a0a0a0',
|
||||||
|
accent: '#6366f1',
|
||||||
|
accentHover: '#4f46e5',
|
||||||
|
userMsg: '#4f46e5',
|
||||||
|
assistantMsg: '#2f2f2f',
|
||||||
|
border: '#3a3a3a',
|
||||||
|
gradient: 'linear-gradient(135deg, #1a1a1a 0%, #2a1a2a 100%)',
|
||||||
|
glow: 'rgba(99, 102, 241, 0.1)'
|
||||||
|
},
|
||||||
|
darker: {
|
||||||
|
name: 'Darker',
|
||||||
|
bgPrimary: '#0a0a0a',
|
||||||
|
bgSecondary: '#141414',
|
||||||
|
bgTertiary: '#1a1a1a',
|
||||||
|
textPrimary: '#e0e0e0',
|
||||||
|
textSecondary: '#909090',
|
||||||
|
accent: '#7c3aed',
|
||||||
|
accentHover: '#6d28d9',
|
||||||
|
userMsg: '#6d28d9',
|
||||||
|
assistantMsg: '#1a1a1a',
|
||||||
|
border: '#2a2a2a',
|
||||||
|
gradient: 'linear-gradient(135deg, #0a0a0a 0%, #1a0a1a 100%)',
|
||||||
|
glow: 'rgba(124, 58, 237, 0.1)'
|
||||||
|
},
|
||||||
|
midnight: {
|
||||||
|
name: 'Midnight Blue',
|
||||||
|
bgPrimary: '#0f1419',
|
||||||
|
bgSecondary: '#1a2332',
|
||||||
|
bgTertiary: '#243447',
|
||||||
|
textPrimary: '#e6f1ff',
|
||||||
|
textSecondary: '#8892a0',
|
||||||
|
accent: '#3b82f6',
|
||||||
|
accentHover: '#2563eb',
|
||||||
|
userMsg: '#1e40af',
|
||||||
|
assistantMsg: '#243447',
|
||||||
|
border: '#2d3e54',
|
||||||
|
gradient: 'linear-gradient(135deg, #0f1419 0%, #1a2845 100%)',
|
||||||
|
glow: 'rgba(59, 130, 246, 0.1)'
|
||||||
|
},
|
||||||
|
forest: {
|
||||||
|
name: 'Forest',
|
||||||
|
bgPrimary: '#0d1b14',
|
||||||
|
bgSecondary: '#162820',
|
||||||
|
bgTertiary: '#1f352b',
|
||||||
|
textPrimary: '#e8f5e9',
|
||||||
|
textSecondary: '#90a89f',
|
||||||
|
accent: '#10b981',
|
||||||
|
accentHover: '#059669',
|
||||||
|
userMsg: '#047857',
|
||||||
|
assistantMsg: '#1f352b',
|
||||||
|
border: '#2d4a3a',
|
||||||
|
gradient: 'linear-gradient(135deg, #0d1b14 0%, #1a2820 100%)',
|
||||||
|
glow: 'rgba(16, 185, 129, 0.1)'
|
||||||
|
},
|
||||||
|
sunset: {
|
||||||
|
name: 'Sunset',
|
||||||
|
bgPrimary: '#1a1214',
|
||||||
|
bgSecondary: '#261a1e',
|
||||||
|
bgTertiary: '#332228',
|
||||||
|
textPrimary: '#fde8e8',
|
||||||
|
textSecondary: '#b89090',
|
||||||
|
accent: '#f97316',
|
||||||
|
accentHover: '#ea580c',
|
||||||
|
userMsg: '#c2410c',
|
||||||
|
assistantMsg: '#332228',
|
||||||
|
border: '#4a3238',
|
||||||
|
gradient: 'linear-gradient(135deg, #1a1214 0%, #2a1a1e 100%)',
|
||||||
|
glow: 'rgba(249, 115, 22, 0.1)'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
name: 'Light',
|
||||||
|
bgPrimary: '#ffffff',
|
||||||
|
bgSecondary: '#f5f5f5',
|
||||||
|
bgTertiary: '#e8e8e8',
|
||||||
|
textPrimary: '#1a1a1a',
|
||||||
|
textSecondary: '#666666',
|
||||||
|
accent: '#6366f1',
|
||||||
|
accentHover: '#4f46e5',
|
||||||
|
userMsg: '#6366f1',
|
||||||
|
assistantMsg: '#f0f0f0',
|
||||||
|
border: '#d0d0d0',
|
||||||
|
gradient: 'linear-gradient(135deg, #ffffff 0%, #f5f0ff 100%)',
|
||||||
|
glow: 'rgba(99, 102, 241, 0.05)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply theme
|
||||||
|
function applyTheme(themeName) {
|
||||||
|
const theme = themes[themeName];
|
||||||
|
if (!theme) return;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty('--bg-primary', theme.bgPrimary);
|
||||||
|
root.style.setProperty('--bg-secondary', theme.bgSecondary);
|
||||||
|
root.style.setProperty('--bg-tertiary', theme.bgTertiary);
|
||||||
|
root.style.setProperty('--text-primary', theme.textPrimary);
|
||||||
|
root.style.setProperty('--text-secondary', theme.textSecondary);
|
||||||
|
root.style.setProperty('--accent', theme.accent);
|
||||||
|
root.style.setProperty('--accent-hover', theme.accentHover);
|
||||||
|
root.style.setProperty('--user-msg', theme.userMsg);
|
||||||
|
root.style.setProperty('--assistant-msg', theme.assistantMsg);
|
||||||
|
root.style.setProperty('--border', theme.border);
|
||||||
|
|
||||||
|
// Update gradient and glow
|
||||||
|
const appContainer = document.querySelector('.app-container');
|
||||||
|
if (appContainer) {
|
||||||
|
appContainer.style.background = theme.gradient;
|
||||||
|
const glow = appContainer.querySelector('::before');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store preference
|
||||||
|
localStorage.setItem('claudia-theme', themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved theme
|
||||||
|
function loadSavedTheme() {
|
||||||
|
const savedTheme = localStorage.getItem('claudia-theme') || 'dark';
|
||||||
|
const themeSelect = document.getElementById('theme-select');
|
||||||
|
if (themeSelect) {
|
||||||
|
themeSelect.value = savedTheme;
|
||||||
|
}
|
||||||
|
applyTheme(savedTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply view mode
|
||||||
|
function applyViewMode(mode) {
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
// Remove all view mode classes
|
||||||
|
body.classList.remove('view-compact', 'view-cozy', 'view-comfortable');
|
||||||
|
|
||||||
|
// Add the selected mode
|
||||||
|
body.classList.add(`view-${mode}`);
|
||||||
|
|
||||||
|
// Store preference
|
||||||
|
localStorage.setItem('claudia-view-mode', mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved view mode
|
||||||
|
function loadSavedViewMode() {
|
||||||
|
const savedMode = localStorage.getItem('claudia-view-mode') || 'cozy';
|
||||||
|
const viewModeSelect = document.getElementById('view-mode-select');
|
||||||
|
if (viewModeSelect) {
|
||||||
|
viewModeSelect.value = savedMode;
|
||||||
|
}
|
||||||
|
applyViewMode(savedMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply font size
|
||||||
|
function applyFontSize(scale) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
// Calculate font size based on scale (80-140%)
|
||||||
|
const baseFontSize = 14; // Default base size in px
|
||||||
|
const newFontSize = (baseFontSize * scale) / 100;
|
||||||
|
|
||||||
|
root.style.setProperty('--base-font-size', `${newFontSize}px`);
|
||||||
|
root.style.fontSize = `${newFontSize}px`;
|
||||||
|
|
||||||
|
// Update the display value
|
||||||
|
const fontSizeValue = document.getElementById('font-size-value');
|
||||||
|
if (fontSizeValue) {
|
||||||
|
fontSizeValue.textContent = `${scale}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store preference
|
||||||
|
localStorage.setItem('claudia-font-size', scale.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved font size
|
||||||
|
function loadSavedFontSize() {
|
||||||
|
const savedSize = parseInt(localStorage.getItem('claudia-font-size') || '100');
|
||||||
|
const fontSizeSlider = document.getElementById('font-size-slider');
|
||||||
|
if (fontSizeSlider) {
|
||||||
|
fontSizeSlider.value = savedSize;
|
||||||
|
}
|
||||||
|
applyFontSize(savedSize);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to get avatar URL
|
// Helper function to get avatar URL
|
||||||
async function getAvatarUrl(avatarFilename) {
|
async function getAvatarUrl(avatarFilename) {
|
||||||
if (!avatarFilename) return null;
|
if (!avatarFilename) return null;
|
||||||
@@ -73,14 +260,82 @@ function makeAvatarClickable(avatarElement, avatarUrl) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format timestamp for display
|
||||||
|
function formatTimestamp(timestamp) {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now - date;
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
// Just now (less than 1 minute)
|
||||||
|
if (seconds < 60) {
|
||||||
|
return 'Just now';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minutes ago (less than 1 hour)
|
||||||
|
if (minutes < 60) {
|
||||||
|
return `${minutes}m ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Today (show time)
|
||||||
|
if (days === 0) {
|
||||||
|
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yesterday
|
||||||
|
if (days === 1) {
|
||||||
|
return `Yesterday at ${date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This week (show day name)
|
||||||
|
if (days < 7) {
|
||||||
|
return date.toLocaleDateString('en-US', { weekday: 'short', hour: 'numeric', minute: '2-digit', hour12: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Older (show date)
|
||||||
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true });
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-resize textarea
|
// Auto-resize textarea
|
||||||
function autoResize(textarea) {
|
function autoResize(textarea) {
|
||||||
textarea.style.height = 'auto';
|
textarea.style.height = 'auto';
|
||||||
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to render assistant message content with character name
|
||||||
|
function renderAssistantContent(contentDiv, messageText) {
|
||||||
|
// Clear existing content
|
||||||
|
contentDiv.innerHTML = '';
|
||||||
|
|
||||||
|
// Add character name indicator
|
||||||
|
if (currentCharacter && currentCharacter.name) {
|
||||||
|
const nameIndicator = document.createElement('div');
|
||||||
|
nameIndicator.className = 'character-name-indicator';
|
||||||
|
nameIndicator.textContent = currentCharacter.name;
|
||||||
|
contentDiv.appendChild(nameIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message content
|
||||||
|
const messageContent = document.createElement('div');
|
||||||
|
messageContent.innerHTML = marked.parse(messageText);
|
||||||
|
contentDiv.appendChild(messageContent);
|
||||||
|
|
||||||
|
// Apply syntax highlighting to code blocks
|
||||||
|
messageContent.querySelectorAll('pre code').forEach((block) => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
addCopyButtonToCode(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
return messageContent;
|
||||||
|
}
|
||||||
|
|
||||||
// Add message to chat
|
// Add message to chat
|
||||||
function addMessage(content, isUser = false, skipActions = false) {
|
function addMessage(content, isUser = false, skipActions = false, timestamp = null) {
|
||||||
const messageDiv = document.createElement('div');
|
const messageDiv = document.createElement('div');
|
||||||
messageDiv.className = `message ${isUser ? 'user' : 'assistant'}`;
|
messageDiv.className = `message ${isUser ? 'user' : 'assistant'}`;
|
||||||
|
|
||||||
@@ -105,12 +360,30 @@ function addMessage(content, isUser = false, skipActions = false) {
|
|||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.textContent = content;
|
p.textContent = content;
|
||||||
contentDiv.appendChild(p);
|
contentDiv.appendChild(p);
|
||||||
|
|
||||||
|
// Add timestamp if provided
|
||||||
|
if (timestamp) {
|
||||||
|
const timestampDiv = document.createElement('div');
|
||||||
|
timestampDiv.className = 'message-timestamp';
|
||||||
|
timestampDiv.textContent = formatTimestamp(timestamp);
|
||||||
|
contentDiv.appendChild(timestampDiv);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Assistant messages: render as markdown
|
// Assistant messages: render as markdown
|
||||||
contentDiv.innerHTML = marked.parse(content);
|
// Add character name indicator if character exists
|
||||||
|
if (currentCharacter && currentCharacter.name) {
|
||||||
|
const nameIndicator = document.createElement('div');
|
||||||
|
nameIndicator.className = 'character-name-indicator';
|
||||||
|
nameIndicator.textContent = currentCharacter.name;
|
||||||
|
contentDiv.appendChild(nameIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageContent = document.createElement('div');
|
||||||
|
messageContent.innerHTML = marked.parse(content);
|
||||||
|
contentDiv.appendChild(messageContent);
|
||||||
|
|
||||||
// Apply syntax highlighting to code blocks
|
// Apply syntax highlighting to code blocks
|
||||||
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
messageContent.querySelectorAll('pre code').forEach((block) => {
|
||||||
hljs.highlightElement(block);
|
hljs.highlightElement(block);
|
||||||
|
|
||||||
// Add copy button to code blocks
|
// Add copy button to code blocks
|
||||||
@@ -141,6 +414,14 @@ function addMessage(content, isUser = false, skipActions = false) {
|
|||||||
pre.appendChild(copyBtn);
|
pre.appendChild(copyBtn);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add timestamp if provided
|
||||||
|
if (timestamp) {
|
||||||
|
const timestampDiv = document.createElement('div');
|
||||||
|
timestampDiv.className = 'message-timestamp';
|
||||||
|
timestampDiv.textContent = formatTimestamp(timestamp);
|
||||||
|
contentDiv.appendChild(timestampDiv);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build message structure
|
// Build message structure
|
||||||
@@ -269,40 +550,7 @@ async function handleSwipeNavigation(messageDiv, direction) {
|
|||||||
const contentDiv = messageDiv.querySelector('.message-content');
|
const contentDiv = messageDiv.querySelector('.message-content');
|
||||||
console.log('Found contentDiv:', contentDiv);
|
console.log('Found contentDiv:', contentDiv);
|
||||||
console.log('Setting content to:', swipeInfo.content);
|
console.log('Setting content to:', swipeInfo.content);
|
||||||
contentDiv.innerHTML = marked.parse(swipeInfo.content);
|
renderAssistantContent(contentDiv, swipeInfo.content);
|
||||||
|
|
||||||
// Apply syntax highlighting to code blocks
|
|
||||||
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
|
|
||||||
// Add copy button
|
|
||||||
const pre = block.parentElement;
|
|
||||||
if (!pre.querySelector('.copy-btn')) {
|
|
||||||
const copyBtn = document.createElement('button');
|
|
||||||
copyBtn.className = 'copy-btn';
|
|
||||||
copyBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
||||||
<rect x="5" y="5" width="9" height="9" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/>
|
|
||||||
<path d="M3 11V3a1 1 0 0 1 1-1h8" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
|
||||||
</svg>`;
|
|
||||||
copyBtn.title = 'Copy code';
|
|
||||||
copyBtn.addEventListener('click', () => {
|
|
||||||
navigator.clipboard.writeText(block.textContent);
|
|
||||||
copyBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
||||||
<path d="M3 8l3 3 7-7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>`;
|
|
||||||
copyBtn.classList.add('copied');
|
|
||||||
setTimeout(() => {
|
|
||||||
copyBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
||||||
<rect x="5" y="5" width="9" height="9" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/>
|
|
||||||
<path d="M3 11V3a1 1 0 0 1 1-1h8" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
|
||||||
</svg>`;
|
|
||||||
copyBtn.classList.remove('copied');
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
pre.style.position = 'relative';
|
|
||||||
pre.appendChild(copyBtn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update swipe controls
|
// Update swipe controls
|
||||||
updateSwipeControls(messageDiv, swipeInfo.current, swipeInfo.total);
|
updateSwipeControls(messageDiv, swipeInfo.current, swipeInfo.total);
|
||||||
@@ -408,6 +656,7 @@ async function handleEditMessage(messageDiv, originalContent) {
|
|||||||
async function handleRegenerateMessage(messageDiv) {
|
async function handleRegenerateMessage(messageDiv) {
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
regenerateBtn.disabled = true;
|
regenerateBtn.disabled = true;
|
||||||
|
regenerateBtn.classList.add('loading');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the last user message
|
// Get the last user message
|
||||||
@@ -418,13 +667,14 @@ async function handleRegenerateMessage(messageDiv) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to regenerate message:', error);
|
console.error('Failed to regenerate message:', error);
|
||||||
regenerateBtn.disabled = false;
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
addMessage(`Error regenerating message: ${error}`, false);
|
addMessage(`Error regenerating message: ${error}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new swipe for an existing assistant message
|
// Generate a new swipe for an existing assistant message
|
||||||
async function generateSwipe(messageDiv, userMessage) {
|
async function generateSwipe(messageDiv, userMessage) {
|
||||||
setStatus('Regenerating...');
|
setStatus('Regenerating response...', 'default');
|
||||||
|
|
||||||
// Check if streaming is enabled
|
// Check if streaming is enabled
|
||||||
let streamEnabled = false;
|
let streamEnabled = false;
|
||||||
@@ -452,32 +702,32 @@ async function generateSwipeNonStream(messageDiv, userMessage) {
|
|||||||
|
|
||||||
// Update the message content
|
// Update the message content
|
||||||
const contentDiv = messageDiv.querySelector('.message-content');
|
const contentDiv = messageDiv.querySelector('.message-content');
|
||||||
contentDiv.innerHTML = marked.parse(swipeInfo.content);
|
renderAssistantContent(contentDiv, swipeInfo.content);
|
||||||
|
|
||||||
// Apply syntax highlighting
|
|
||||||
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
addCopyButtonToCode(block);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update swipe controls
|
// Update swipe controls
|
||||||
updateSwipeControls(messageDiv, swipeInfo.current, swipeInfo.total);
|
updateSwipeControls(messageDiv, swipeInfo.current, swipeInfo.total);
|
||||||
|
|
||||||
setStatus('Ready');
|
setStatus('Regeneration complete', 'success');
|
||||||
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
if (regenerateBtn) {
|
||||||
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus('Error');
|
setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
if (regenerateBtn) {
|
||||||
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
addMessage(`Error regenerating message: ${error}`, false);
|
addMessage(`Error regenerating message: ${error}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate swipe using streaming
|
// Generate swipe using streaming
|
||||||
async function generateSwipeStream(messageDiv, userMessage) {
|
async function generateSwipeStream(messageDiv, userMessage) {
|
||||||
setStatus('Streaming...');
|
setStatus('Streaming regeneration...', 'streaming');
|
||||||
statusText.classList.add('streaming');
|
|
||||||
|
|
||||||
let fullContent = '';
|
let fullContent = '';
|
||||||
const contentDiv = messageDiv.querySelector('.message-content');
|
const contentDiv = messageDiv.querySelector('.message-content');
|
||||||
@@ -490,13 +740,7 @@ async function generateSwipeStream(messageDiv, userMessage) {
|
|||||||
fullContent += token;
|
fullContent += token;
|
||||||
|
|
||||||
// Update content with markdown rendering
|
// Update content with markdown rendering
|
||||||
contentDiv.innerHTML = marked.parse(fullContent);
|
renderAssistantContent(contentDiv, fullContent);
|
||||||
|
|
||||||
// Apply syntax highlighting
|
|
||||||
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
addCopyButtonToCode(block);
|
|
||||||
});
|
|
||||||
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
});
|
});
|
||||||
@@ -512,10 +756,13 @@ async function generateSwipeStream(messageDiv, userMessage) {
|
|||||||
console.error('Failed to add swipe:', error);
|
console.error('Failed to add swipe:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('Ready');
|
setStatus('Regeneration complete', 'success');
|
||||||
statusText.classList.remove('streaming');
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
if (regenerateBtn) {
|
||||||
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
tokenUnlisten();
|
tokenUnlisten();
|
||||||
completeUnlisten();
|
completeUnlisten();
|
||||||
});
|
});
|
||||||
@@ -525,10 +772,12 @@ async function generateSwipeStream(messageDiv, userMessage) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
tokenUnlisten();
|
tokenUnlisten();
|
||||||
completeUnlisten();
|
completeUnlisten();
|
||||||
statusText.classList.remove('streaming');
|
setStatus(`Regeneration failed: ${error.substring(0, 40)}...`, 'error');
|
||||||
setStatus('Error');
|
|
||||||
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
const regenerateBtn = messageDiv.querySelector('.message-action-btn');
|
||||||
if (regenerateBtn) regenerateBtn.disabled = false;
|
if (regenerateBtn) {
|
||||||
|
regenerateBtn.disabled = false;
|
||||||
|
regenerateBtn.classList.remove('loading');
|
||||||
|
}
|
||||||
addMessage(`Error: ${error}`, false);
|
addMessage(`Error: ${error}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,12 +815,12 @@ function addCopyButtonToCode(block) {
|
|||||||
// Extract message sending logic into separate function
|
// Extract message sending logic into separate function
|
||||||
async function sendMessage(message, isRegenerate = false) {
|
async function sendMessage(message, isRegenerate = false) {
|
||||||
if (!isRegenerate) {
|
if (!isRegenerate) {
|
||||||
addMessage(message, true);
|
addMessage(message, true, false, Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
sendBtn.disabled = true;
|
sendBtn.disabled = true;
|
||||||
messageInput.disabled = true;
|
messageInput.disabled = true;
|
||||||
setStatus('Thinking...');
|
setStatus('Connecting to API...', 'default');
|
||||||
|
|
||||||
// Check if streaming is enabled
|
// Check if streaming is enabled
|
||||||
let streamEnabled = false;
|
let streamEnabled = false;
|
||||||
@@ -584,8 +833,7 @@ async function sendMessage(message, isRegenerate = false) {
|
|||||||
|
|
||||||
if (streamEnabled) {
|
if (streamEnabled) {
|
||||||
// Use streaming
|
// Use streaming
|
||||||
setStatus('Streaming...');
|
setStatus('Streaming response...', 'streaming');
|
||||||
statusText.classList.add('streaming');
|
|
||||||
|
|
||||||
let streamingMessageDiv = null;
|
let streamingMessageDiv = null;
|
||||||
let streamingContentDiv = null;
|
let streamingContentDiv = null;
|
||||||
@@ -635,40 +883,7 @@ async function sendMessage(message, isRegenerate = false) {
|
|||||||
fullContent += token;
|
fullContent += token;
|
||||||
|
|
||||||
// Update content with markdown rendering
|
// Update content with markdown rendering
|
||||||
streamingContentDiv.innerHTML = marked.parse(fullContent);
|
renderAssistantContent(streamingContentDiv, fullContent);
|
||||||
|
|
||||||
// Apply syntax highlighting
|
|
||||||
streamingContentDiv.querySelectorAll('pre code').forEach((block) => {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
|
|
||||||
// Add copy button
|
|
||||||
const pre = block.parentElement;
|
|
||||||
if (!pre.querySelector('.copy-btn')) {
|
|
||||||
const copyBtn = document.createElement('button');
|
|
||||||
copyBtn.className = 'copy-btn';
|
|
||||||
copyBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
||||||
<rect x="5" y="5" width="9" height="9" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/>
|
|
||||||
<path d="M3 11V3a1 1 0 0 1 1-1h8" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
|
||||||
</svg>`;
|
|
||||||
copyBtn.title = 'Copy code';
|
|
||||||
copyBtn.addEventListener('click', () => {
|
|
||||||
navigator.clipboard.writeText(block.textContent);
|
|
||||||
copyBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
||||||
<path d="M3 8l3 3 7-7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>`;
|
|
||||||
copyBtn.classList.add('copied');
|
|
||||||
setTimeout(() => {
|
|
||||||
copyBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
||||||
<rect x="5" y="5" width="9" height="9" stroke="currentColor" stroke-width="1.5" fill="none" rx="1"/>
|
|
||||||
<path d="M3 11V3a1 1 0 0 1 1-1h8" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
|
||||||
</svg>`;
|
|
||||||
copyBtn.classList.remove('copied');
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
pre.style.position = 'relative';
|
|
||||||
pre.appendChild(copyBtn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
});
|
});
|
||||||
@@ -689,8 +904,8 @@ async function sendMessage(message, isRegenerate = false) {
|
|||||||
actionsDiv.appendChild(regenerateBtn);
|
actionsDiv.appendChild(regenerateBtn);
|
||||||
streamingMessageDiv.appendChild(actionsDiv);
|
streamingMessageDiv.appendChild(actionsDiv);
|
||||||
|
|
||||||
setStatus('Ready');
|
setStatus('Response complete', 'success');
|
||||||
statusText.classList.remove('streaming');
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
sendBtn.disabled = false;
|
sendBtn.disabled = false;
|
||||||
messageInput.disabled = false;
|
messageInput.disabled = false;
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
@@ -703,17 +918,17 @@ async function sendMessage(message, isRegenerate = false) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
tokenUnlisten();
|
tokenUnlisten();
|
||||||
completeUnlisten();
|
completeUnlisten();
|
||||||
statusText.classList.remove('streaming');
|
|
||||||
if (streamingMessageDiv) {
|
if (streamingMessageDiv) {
|
||||||
streamingMessageDiv.remove();
|
streamingMessageDiv.remove();
|
||||||
}
|
}
|
||||||
if (error.includes('not configured')) {
|
if (error.includes('not configured')) {
|
||||||
addMessage('API not configured. Please configure your API settings.', false);
|
addMessage('API not configured. Please configure your API settings.', false);
|
||||||
|
setStatus('API not configured', 'error');
|
||||||
setTimeout(showSettings, 1000);
|
setTimeout(showSettings, 1000);
|
||||||
} else {
|
} else {
|
||||||
addMessage(`Error: ${error}`, false);
|
addMessage(`Error: ${error}`, false);
|
||||||
|
setStatus(`Error: ${error.substring(0, 50)}...`, 'error');
|
||||||
}
|
}
|
||||||
setStatus('Error');
|
|
||||||
sendBtn.disabled = false;
|
sendBtn.disabled = false;
|
||||||
messageInput.disabled = false;
|
messageInput.disabled = false;
|
||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
@@ -726,16 +941,18 @@ async function sendMessage(message, isRegenerate = false) {
|
|||||||
const response = await invoke('chat', { message });
|
const response = await invoke('chat', { message });
|
||||||
removeTypingIndicator();
|
removeTypingIndicator();
|
||||||
addMessage(response, false);
|
addMessage(response, false);
|
||||||
setStatus('Ready');
|
setStatus('Response complete', 'success');
|
||||||
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
removeTypingIndicator();
|
removeTypingIndicator();
|
||||||
if (error.includes('not configured')) {
|
if (error.includes('not configured')) {
|
||||||
addMessage('API not configured. Please configure your API settings.', false);
|
addMessage('API not configured. Please configure your API settings.', false);
|
||||||
|
setStatus('API not configured', 'error');
|
||||||
setTimeout(showSettings, 1000);
|
setTimeout(showSettings, 1000);
|
||||||
} else {
|
} else {
|
||||||
addMessage(`Error: ${error}`, false);
|
addMessage(`Error: ${error}`, false);
|
||||||
|
setStatus(`Error: ${error.substring(0, 50)}...`, 'error');
|
||||||
}
|
}
|
||||||
setStatus('Error');
|
|
||||||
} finally {
|
} finally {
|
||||||
sendBtn.disabled = false;
|
sendBtn.disabled = false;
|
||||||
messageInput.disabled = false;
|
messageInput.disabled = false;
|
||||||
@@ -772,21 +989,35 @@ function removeTypingIndicator() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status
|
// Update status with optional styling
|
||||||
function setStatus(text) {
|
function setStatus(text, type = 'default') {
|
||||||
statusText.textContent = text;
|
statusText.textContent = text;
|
||||||
|
|
||||||
|
// Remove all status classes
|
||||||
|
statusText.classList.remove('streaming', 'error', 'success');
|
||||||
|
|
||||||
|
// Add appropriate class based on type
|
||||||
|
if (type === 'streaming') {
|
||||||
|
statusText.classList.add('streaming');
|
||||||
|
} else if (type === 'error') {
|
||||||
|
statusText.classList.add('error');
|
||||||
|
} else if (type === 'success') {
|
||||||
|
statusText.classList.add('success');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide settings
|
// Show/hide settings
|
||||||
async function showSettings() {
|
async function showSettings() {
|
||||||
settingsPanel.style.display = 'block';
|
const overlay = document.getElementById('settings-overlay');
|
||||||
chatView.style.display = 'none';
|
settingsPanel.classList.add('open');
|
||||||
|
overlay.classList.add('show');
|
||||||
await loadCharacterSettings();
|
await loadCharacterSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideSettings() {
|
function hideSettings() {
|
||||||
settingsPanel.style.display = 'none';
|
const overlay = document.getElementById('settings-overlay');
|
||||||
chatView.style.display = 'flex';
|
settingsPanel.classList.remove('open');
|
||||||
|
overlay.classList.remove('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab switching
|
// Tab switching
|
||||||
@@ -839,14 +1070,18 @@ async function handleValidate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateBtn.disabled = true;
|
validateBtn.disabled = true;
|
||||||
|
validateBtn.classList.add('loading');
|
||||||
validateBtn.textContent = 'Validating...';
|
validateBtn.textContent = 'Validating...';
|
||||||
validationMsg.style.display = 'none';
|
validationMsg.style.display = 'none';
|
||||||
|
setStatus('Validating API...', 'default');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const models = await invoke('validate_api', { baseUrl, apiKey });
|
const models = await invoke('validate_api', { baseUrl, apiKey });
|
||||||
|
|
||||||
validationMsg.textContent = `Found ${models.length} models`;
|
validationMsg.textContent = `Found ${models.length} models`;
|
||||||
validationMsg.className = 'validation-message success';
|
validationMsg.className = 'validation-message success';
|
||||||
|
setStatus('API validated successfully', 'success');
|
||||||
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
|
|
||||||
modelSelect.innerHTML = '<option value="">Select a model</option>';
|
modelSelect.innerHTML = '<option value="">Select a model</option>';
|
||||||
models.forEach(model => {
|
models.forEach(model => {
|
||||||
@@ -857,14 +1092,17 @@ async function handleValidate() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
modelsGroup.style.display = 'flex';
|
modelsGroup.style.display = 'flex';
|
||||||
|
modelsGroup.classList.add('fade-in');
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
validationMsg.textContent = `Validation failed: ${error}`;
|
validationMsg.textContent = `Validation failed: ${error}`;
|
||||||
validationMsg.className = 'validation-message error';
|
validationMsg.className = 'validation-message error';
|
||||||
|
setStatus('API validation failed', 'error');
|
||||||
modelsGroup.style.display = 'none';
|
modelsGroup.style.display = 'none';
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
} finally {
|
} finally {
|
||||||
validateBtn.disabled = false;
|
validateBtn.disabled = false;
|
||||||
|
validateBtn.classList.remove('loading');
|
||||||
validateBtn.textContent = 'Validate';
|
validateBtn.textContent = 'Validate';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -886,23 +1124,29 @@ async function handleSaveSettings(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.classList.add('loading');
|
||||||
saveBtn.textContent = 'Saving...';
|
saveBtn.textContent = 'Saving...';
|
||||||
|
setStatus('Saving configuration...', 'default');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invoke('save_api_config', { baseUrl, apiKey, model, stream });
|
await invoke('save_api_config', { baseUrl, apiKey, model, stream });
|
||||||
validationMsg.textContent = 'Configuration saved successfully';
|
validationMsg.textContent = 'Configuration saved successfully';
|
||||||
validationMsg.className = 'validation-message success';
|
validationMsg.className = 'validation-message success';
|
||||||
|
setStatus('Configuration saved', 'success');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hideSettings();
|
hideSettings();
|
||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
addMessage('API configured. Ready to chat.', false, true);
|
addMessage('API configured. Ready to chat.', false, true);
|
||||||
|
setStatus('Ready');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
validationMsg.textContent = `Failed to save: ${error}`;
|
validationMsg.textContent = `Failed to save: ${error}`;
|
||||||
validationMsg.className = 'validation-message error';
|
validationMsg.className = 'validation-message error';
|
||||||
|
setStatus('Failed to save configuration', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.classList.remove('loading');
|
||||||
saveBtn.textContent = 'Save Configuration';
|
saveBtn.textContent = 'Save Configuration';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -960,6 +1204,7 @@ function handleAvatarRemove() {
|
|||||||
function setupAppControls() {
|
function setupAppControls() {
|
||||||
document.getElementById('settings-btn').addEventListener('click', showSettings);
|
document.getElementById('settings-btn').addEventListener('click', showSettings);
|
||||||
document.getElementById('close-settings-btn').addEventListener('click', hideSettings);
|
document.getElementById('close-settings-btn').addEventListener('click', hideSettings);
|
||||||
|
document.getElementById('settings-overlay').addEventListener('click', hideSettings);
|
||||||
document.getElementById('clear-btn').addEventListener('click', clearHistory);
|
document.getElementById('clear-btn').addEventListener('click', clearHistory);
|
||||||
characterSelect.addEventListener('change', handleCharacterSwitch);
|
characterSelect.addEventListener('change', handleCharacterSwitch);
|
||||||
newCharacterBtn.addEventListener('click', handleNewCharacter);
|
newCharacterBtn.addEventListener('click', handleNewCharacter);
|
||||||
@@ -973,6 +1218,38 @@ function setupAppControls() {
|
|||||||
document.getElementById('remove-avatar-btn').addEventListener('click', handleAvatarRemove);
|
document.getElementById('remove-avatar-btn').addEventListener('click', handleAvatarRemove);
|
||||||
document.getElementById('import-character-btn').addEventListener('click', handleImportCharacter);
|
document.getElementById('import-character-btn').addEventListener('click', handleImportCharacter);
|
||||||
document.getElementById('export-character-btn').addEventListener('click', handleExportCharacter);
|
document.getElementById('export-character-btn').addEventListener('click', handleExportCharacter);
|
||||||
|
|
||||||
|
// Setup collapsible sections
|
||||||
|
document.querySelectorAll('.settings-section-header').forEach(header => {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const section = header.parentElement;
|
||||||
|
section.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup theme selector
|
||||||
|
const themeSelect = document.getElementById('theme-select');
|
||||||
|
if (themeSelect) {
|
||||||
|
themeSelect.addEventListener('change', (e) => {
|
||||||
|
applyTheme(e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup view mode selector
|
||||||
|
const viewModeSelect = document.getElementById('view-mode-select');
|
||||||
|
if (viewModeSelect) {
|
||||||
|
viewModeSelect.addEventListener('change', (e) => {
|
||||||
|
applyViewMode(e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup font size slider
|
||||||
|
const fontSizeSlider = document.getElementById('font-size-slider');
|
||||||
|
if (fontSizeSlider) {
|
||||||
|
fontSizeSlider.addEventListener('input', (e) => {
|
||||||
|
applyFontSize(parseInt(e.target.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
@@ -1032,12 +1309,16 @@ async function loadCharacters() {
|
|||||||
// Handle character switching
|
// Handle character switching
|
||||||
async function handleCharacterSwitch() {
|
async function handleCharacterSwitch() {
|
||||||
const characterId = characterSelect.value;
|
const characterId = characterSelect.value;
|
||||||
|
setStatus('Switching character...', 'default');
|
||||||
try {
|
try {
|
||||||
await invoke('set_active_character', { characterId });
|
await invoke('set_active_character', { characterId });
|
||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
await loadCharacters();
|
await loadCharacters();
|
||||||
|
setStatus('Character switched', 'success');
|
||||||
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to switch character:', error);
|
console.error('Failed to switch character:', error);
|
||||||
|
setStatus('Failed to switch character', 'error');
|
||||||
addMessage(`Failed to switch character: ${error}`, false);
|
addMessage(`Failed to switch character: ${error}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1138,7 +1419,7 @@ async function loadChatHistory() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
history.forEach((msg, index) => {
|
history.forEach((msg, index) => {
|
||||||
const messageDiv = addMessage(msg.content, msg.role === 'user');
|
const messageDiv = addMessage(msg.content, msg.role === 'user', false, msg.timestamp);
|
||||||
|
|
||||||
// Update swipe controls for assistant messages with swipe info
|
// Update swipe controls for assistant messages with swipe info
|
||||||
if (msg.role === 'assistant' && messageDiv && msg.swipes && msg.swipes.length > 0) {
|
if (msg.role === 'assistant' && messageDiv && msg.swipes && msg.swipes.length > 0) {
|
||||||
@@ -1159,6 +1440,7 @@ async function clearHistory() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStatus('Clearing history...', 'default');
|
||||||
try {
|
try {
|
||||||
await invoke('clear_chat_history');
|
await invoke('clear_chat_history');
|
||||||
messagesContainer.innerHTML = '';
|
messagesContainer.innerHTML = '';
|
||||||
@@ -1167,7 +1449,10 @@ async function clearHistory() {
|
|||||||
} else {
|
} else {
|
||||||
addMessage('Conversation cleared. Ready to chat.', false, true);
|
addMessage('Conversation cleared. Ready to chat.', false, true);
|
||||||
}
|
}
|
||||||
|
setStatus('History cleared', 'success');
|
||||||
|
setTimeout(() => setStatus('Ready'), 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setStatus('Failed to clear history', 'error');
|
||||||
addMessage(`Failed to clear history: ${error}`, false);
|
addMessage(`Failed to clear history: ${error}`, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1252,6 +1537,7 @@ async function handleSaveCharacter(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.classList.add('loading');
|
||||||
saveBtn.textContent = 'Saving...';
|
saveBtn.textContent = 'Saving...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1284,6 +1570,7 @@ async function handleSaveCharacter(e) {
|
|||||||
characterMsg.className = 'validation-message error';
|
characterMsg.className = 'validation-message error';
|
||||||
} finally {
|
} finally {
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.classList.remove('loading');
|
||||||
saveBtn.textContent = 'Save Character';
|
saveBtn.textContent = 'Save Character';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1357,5 +1644,10 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
messageInput.focus();
|
messageInput.focus();
|
||||||
setStatus('Ready');
|
setStatus('Ready');
|
||||||
|
|
||||||
|
// Load saved preferences before anything else
|
||||||
|
loadSavedTheme();
|
||||||
|
loadSavedViewMode();
|
||||||
|
loadSavedFontSize();
|
||||||
|
|
||||||
loadExistingConfig();
|
loadExistingConfig();
|
||||||
});
|
});
|
||||||
|
|||||||
566
src/styles.css
566
src/styles.css
@@ -86,6 +86,7 @@ body {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-circle-large {
|
.avatar-circle-large {
|
||||||
@@ -245,6 +246,19 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.message.user {
|
.message.user {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
@@ -287,6 +301,34 @@ body {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Message timestamp */
|
||||||
|
.message-timestamp {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-top: 4px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-timestamp {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.assistant .message-timestamp {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Character name indicator */
|
||||||
|
.character-name-indicator {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
opacity: 0.9;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Message action buttons */
|
/* Message action buttons */
|
||||||
.message-actions {
|
.message-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -703,6 +745,7 @@ body {
|
|||||||
.status-text {
|
.status-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-text.streaming {
|
.status-text.streaming {
|
||||||
@@ -710,6 +753,14 @@ body {
|
|||||||
animation: pulse 2s ease-in-out infinite;
|
animation: pulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-text.error {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.success {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -719,9 +770,66 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light mode support */
|
/* Loading spinner */
|
||||||
@media (prefers-color-scheme: light) {
|
.loading-spinner {
|
||||||
:root {
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: currentColor;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button loading state */
|
||||||
|
.btn-primary.loading,
|
||||||
|
.btn-secondary.loading,
|
||||||
|
.message-action-btn.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: wait;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary.loading::after,
|
||||||
|
.btn-secondary.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: white;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme System */
|
||||||
|
/* Dark theme (default) */
|
||||||
|
:root,
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg-primary: #1a1a1a;
|
||||||
|
--bg-secondary: #252525;
|
||||||
|
--bg-tertiary: #2f2f2f;
|
||||||
|
--text-primary: #e8e8e8;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--accent: #6366f1;
|
||||||
|
--accent-hover: #4f46e5;
|
||||||
|
--user-msg: #4f46e5;
|
||||||
|
--assistant-msg: #2f2f2f;
|
||||||
|
--border: #3a3a3a;
|
||||||
|
--shadow: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme */
|
||||||
|
[data-theme="light"] {
|
||||||
--bg-primary: #ffffff;
|
--bg-primary: #ffffff;
|
||||||
--bg-secondary: #f5f5f5;
|
--bg-secondary: #f5f5f5;
|
||||||
--bg-tertiary: #e8e8e8;
|
--bg-tertiary: #e8e8e8;
|
||||||
@@ -733,35 +841,139 @@ body {
|
|||||||
--assistant-msg: #f5f5f5;
|
--assistant-msg: #f5f5f5;
|
||||||
--border: #e0e0e0;
|
--border: #e0e0e0;
|
||||||
--shadow: rgba(0, 0, 0, 0.1);
|
--shadow: rgba(0, 0, 0, 0.1);
|
||||||
}
|
|
||||||
|
|
||||||
.message.assistant .message-content {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Settings Panel */
|
[data-theme="light"] .app-container {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .app-container::before {
|
||||||
|
background: radial-gradient(circle at top center, rgba(99, 102, 241, 0.05) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .message.assistant .message-content {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Abyss theme - Deep purple/blue */
|
||||||
|
[data-theme="abyss"] {
|
||||||
|
--bg-primary: #0f0f1e;
|
||||||
|
--bg-secondary: #1a1a2e;
|
||||||
|
--bg-tertiary: #252538;
|
||||||
|
--text-primary: #e0e0ff;
|
||||||
|
--text-secondary: #8888bb;
|
||||||
|
--accent: #9d4edd;
|
||||||
|
--accent-hover: #7b2cbf;
|
||||||
|
--user-msg: #7b2cbf;
|
||||||
|
--assistant-msg: #252538;
|
||||||
|
--border: #2f2f48;
|
||||||
|
--shadow: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="abyss"] .app-container {
|
||||||
|
background: linear-gradient(135deg, #0f0f1e 0%, #1a1033 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="abyss"] .app-container::before {
|
||||||
|
background: radial-gradient(circle at top center, rgba(157, 78, 221, 0.15) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nord theme - Cool blues and grays */
|
||||||
|
[data-theme="nord"] {
|
||||||
|
--bg-primary: #2e3440;
|
||||||
|
--bg-secondary: #3b4252;
|
||||||
|
--bg-tertiary: #434c5e;
|
||||||
|
--text-primary: #eceff4;
|
||||||
|
--text-secondary: #d8dee9;
|
||||||
|
--accent: #88c0d0;
|
||||||
|
--accent-hover: #81a1c1;
|
||||||
|
--user-msg: #5e81ac;
|
||||||
|
--assistant-msg: #434c5e;
|
||||||
|
--border: #4c566a;
|
||||||
|
--shadow: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="nord"] .app-container {
|
||||||
|
background: linear-gradient(135deg, #2e3440 0%, #3b4252 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="nord"] .app-container::before {
|
||||||
|
background: radial-gradient(circle at top center, rgba(136, 192, 208, 0.1) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mocha theme - Warm browns */
|
||||||
|
[data-theme="mocha"] {
|
||||||
|
--bg-primary: #1e1e2e;
|
||||||
|
--bg-secondary: #2a2837;
|
||||||
|
--bg-tertiary: #3a3850;
|
||||||
|
--text-primary: #cdd6f4;
|
||||||
|
--text-secondary: #bac2de;
|
||||||
|
--accent: #f5c2e7;
|
||||||
|
--accent-hover: #cba6f7;
|
||||||
|
--user-msg: #cba6f7;
|
||||||
|
--assistant-msg: #3a3850;
|
||||||
|
--border: #45475a;
|
||||||
|
--shadow: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="mocha"] .app-container {
|
||||||
|
background: linear-gradient(135deg, #1e1e2e 0%, #2a2440 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="mocha"] .app-container::before {
|
||||||
|
background: radial-gradient(circle at top center, rgba(245, 194, 231, 0.1) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Panel - Slide-in Sidebar */
|
||||||
.settings-panel {
|
.settings-panel {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: -500px;
|
||||||
|
width: 500px;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel.open {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings overlay backdrop */
|
||||||
|
.settings-overlay {
|
||||||
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: var(--bg-primary);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 1000;
|
z-index: 999;
|
||||||
overflow-y: auto;
|
opacity: 0;
|
||||||
padding: 60px 20px 20px;
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-overlay.show {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header {
|
.settings-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header h2 {
|
.settings-header h2 {
|
||||||
font-size: 20px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -771,9 +983,6 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
max-width: 400px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn {
|
.tab-btn {
|
||||||
@@ -810,13 +1019,68 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-form {
|
.settings-form {
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Collapsible sections */
|
||||||
|
.settings-section {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-header:hover {
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section.collapsed .settings-section-icon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-content {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-height: 2000px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease, padding 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section.collapsed .settings-section-content {
|
||||||
|
max-height: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -988,6 +1252,259 @@ body {
|
|||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Theme Preview */
|
||||||
|
.theme-preview-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview-message.user-preview {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview-message.assistant-preview {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview-avatar {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview-content {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview-message.user-preview .theme-preview-content {
|
||||||
|
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-preview-message.assistant-preview .theme-preview-content {
|
||||||
|
background: var(--assistant-msg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Font size slider */
|
||||||
|
.font-size-slider {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-slider::-webkit-slider-thumb:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-slider::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-slider::-moz-range-thumb:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-size-slider::-moz-range-track {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#font-size-value {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* View Mode Styles */
|
||||||
|
|
||||||
|
/* Compact Mode */
|
||||||
|
body.view-compact .messages-list {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .message-content {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .avatar-circle {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .character-name-indicator {
|
||||||
|
font-size: 10px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .message-timestamp {
|
||||||
|
font-size: 9px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .swipe-controls {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .message-content h1 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .message-content h2 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .message-content h3 {
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-compact .message-content code {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cozy Mode (Default) - Already defined in base styles */
|
||||||
|
body.view-cozy .messages-list {
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comfortable Mode */
|
||||||
|
body.view-comfortable .messages-list {
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .message-content {
|
||||||
|
padding: 16px 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.7;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .avatar-circle {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .character-name-indicator {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .message-timestamp {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .swipe-controls {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .message-content h1 {
|
||||||
|
font-size: 1.6em;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .message-content h2 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
margin-top: 18px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .message-content h3 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .message-content p + p {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .message-content code {
|
||||||
|
font-size: 0.95em;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.view-comfortable .message-content pre {
|
||||||
|
padding: 16px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.messages-list {
|
.messages-list {
|
||||||
@@ -1003,6 +1520,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-panel {
|
.settings-panel {
|
||||||
padding: 60px 16px 16px;
|
width: 100%;
|
||||||
|
right: -100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel.open {
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user