diff --git a/server/routes/auth.js b/server/routes/auth.js index ab04236..82a7c0d 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -1,6 +1,6 @@ import express from 'express'; import bcrypt from 'bcrypt'; -import { userDb } from '../database/db.js'; +import { userDb, db } from '../database/db.js'; import { generateToken, authenticateToken } from '../middleware/auth.js'; const router = express.Router(); @@ -33,31 +33,41 @@ router.post('/register', async (req, res) => { return res.status(400).json({ error: 'Username must be at least 3 characters, password at least 6 characters' }); } - // Check if users already exist (only allow one user) - const hasUsers = userDb.hasUsers(); - if (hasUsers) { - return res.status(403).json({ error: 'User already exists. This is a single-user system.' }); + // Use a transaction to prevent race conditions + db.prepare('BEGIN').run(); + try { + // Check if users already exist (only allow one user) + const hasUsers = userDb.hasUsers(); + if (hasUsers) { + db.prepare('ROLLBACK').run(); + return res.status(403).json({ error: 'User already exists. This is a single-user system.' }); + } + + // Hash password + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + + // Create user + const user = userDb.createUser(username, passwordHash); + + // Generate token + const token = generateToken(user); + + // Update last login + userDb.updateLastLogin(user.id); + + db.prepare('COMMIT').run(); + + res.json({ + success: true, + user: { id: user.id, username: user.username }, + token + }); + } catch (error) { + db.prepare('ROLLBACK').run(); + throw error; } - // Hash password - const saltRounds = 12; - const passwordHash = await bcrypt.hash(password, saltRounds); - - // Create user - const user = userDb.createUser(username, passwordHash); - - // Generate token - const token = generateToken(user); - - // Update last login - userDb.updateLastLogin(user.id); - - res.json({ - success: true, - user: { id: user.id, username: user.username }, - token - }); - } catch (error) { console.error('Registration error:', error); if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { diff --git a/src/App.jsx b/src/App.jsx index 68e6e21..b9a9d8c 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -64,6 +64,10 @@ function AppContent() { const saved = localStorage.getItem('autoScrollToBottom'); return saved !== null ? JSON.parse(saved) : true; }); + const [sendByCtrlEnter, setSendByCtrlEnter] = useState(() => { + const saved = localStorage.getItem('sendByCtrlEnter'); + return saved !== null ? JSON.parse(saved) : false; + }); // Session Protection System: Track sessions with active conversations to prevent // automatic project updates from interrupting ongoing chats. When a user sends // a message, the session is marked as "active" and project updates are paused @@ -586,6 +590,7 @@ function AppContent() { autoExpandTools={autoExpandTools} showRawParameters={showRawParameters} autoScrollToBottom={autoScrollToBottom} + sendByCtrlEnter={sendByCtrlEnter} /> @@ -617,6 +622,11 @@ function AppContent() { setAutoScrollToBottom(value); localStorage.setItem('autoScrollToBottom', JSON.stringify(value)); }} + sendByCtrlEnter={sendByCtrlEnter} + onSendByCtrlEnterChange={(value) => { + setSendByCtrlEnter(value); + localStorage.setItem('sendByCtrlEnter', JSON.stringify(value)); + }} isMobile={isMobile} /> )} diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 186d80b..d61ddd3 100755 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -1098,7 +1098,7 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => { // - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID // // This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations. -function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom }) { +function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter }) { const [input, setInput] = useState(() => { if (typeof window !== 'undefined' && selectedProject) { return safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || ''; @@ -1514,19 +1514,45 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess toolResult: null // Will be updated when result comes in }]); } else if (part.type === 'text' && part.text?.trim()) { + // Check for usage limit message and format it user-friendly + let content = part.text; + if (content.includes('Claude AI usage limit reached|')) { + const parts = content.split('|'); + if (parts.length === 2) { + const timestamp = parseInt(parts[1]); + if (!isNaN(timestamp)) { + const resetTime = new Date(timestamp * 1000); + content = `Claude AI usage limit reached. The limit will reset on ${resetTime.toLocaleDateString()} at ${resetTime.toLocaleTimeString()}.`; + } + } + } + // Add regular text message setChatMessages(prev => [...prev, { type: 'assistant', - content: part.text, + content: content, timestamp: new Date() }]); } } } else if (typeof messageData.content === 'string' && messageData.content.trim()) { + // Check for usage limit message and format it user-friendly + let content = messageData.content; + if (content.includes('Claude AI usage limit reached|')) { + const parts = content.split('|'); + if (parts.length === 2) { + const timestamp = parseInt(parts[1]); + if (!isNaN(timestamp)) { + const resetTime = new Date(timestamp * 1000); + content = `Claude AI usage limit reached. The limit will reset on ${resetTime.toLocaleDateString()} at ${resetTime.toLocaleTimeString()}.`; + } + } + } + // Add regular text message setChatMessages(prev => [...prev, { type: 'assistant', - content: messageData.content, + content: content, timestamp: new Date() }]); } @@ -2099,14 +2125,21 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Handle Enter key: Ctrl+Enter (Cmd+Enter on Mac) sends, Shift+Enter creates new line if (e.key === 'Enter') { + // If we're in composition, don't send message + if (e.nativeEvent.isComposing) { + return; // Let IME handle the Enter key + } + if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { // Ctrl+Enter or Cmd+Enter: Send message e.preventDefault(); handleSubmit(e); } else if (!e.shiftKey && !e.ctrlKey && !e.metaKey) { - // Plain Enter: Also send message (keeping original behavior) - e.preventDefault(); - handleSubmit(e); + // Plain Enter: Send message only if not in IME composition + if (!sendByCtrlEnter) { + e.preventDefault(); + handleSubmit(e); + } } // Shift+Enter: Allow default behavior (new line) } @@ -2537,12 +2570,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess {/* Hint text */}
- Press Enter to send • Shift+Enter for new line • Tab to change modes • @ to reference files + {sendByCtrlEnter + ? "Ctrl+Enter to send (IME safe) • Shift+Enter for new line • Tab to change modes • @ to reference files" + : "Press Enter to send • Shift+Enter for new line • Tab to change modes • @ to reference files"}
- Enter to send • Tab for modes • @ for files + {sendByCtrlEnter + ? "Ctrl+Enter to send (IME safe) • Tab for modes • @ for files" + : "Enter to send • Tab for modes • @ for files"}
diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx index 34d6316..52dc236 100755 --- a/src/components/MainContent.jsx +++ b/src/components/MainContent.jsx @@ -40,7 +40,8 @@ function MainContent({ onShowSettings, // Show tools settings panel autoExpandTools, // Auto-expand tool accordions showRawParameters, // Show raw parameters in tool accordions - autoScrollToBottom // Auto-scroll to bottom when new messages arrive + autoScrollToBottom, // Auto-scroll to bottom when new messages arrive + sendByCtrlEnter // Send by Ctrl+Enter mode for East Asian language input }) { const [editingFile, setEditingFile] = useState(null); @@ -287,6 +288,7 @@ function MainContent({ autoExpandTools={autoExpandTools} showRawParameters={showRawParameters} autoScrollToBottom={autoScrollToBottom} + sendByCtrlEnter={sendByCtrlEnter} /> diff --git a/src/components/QuickSettingsPanel.jsx b/src/components/QuickSettingsPanel.jsx index 6a5d091..76e9514 100755 --- a/src/components/QuickSettingsPanel.jsx +++ b/src/components/QuickSettingsPanel.jsx @@ -11,7 +11,8 @@ import { Mic, Brain, Sparkles, - FileText + FileText, + Languages } from 'lucide-react'; import DarkModeToggle from './DarkModeToggle'; import { useTheme } from '../contexts/ThemeContext'; @@ -25,6 +26,8 @@ const QuickSettingsPanel = ({ onShowRawParametersChange, autoScrollToBottom, onAutoScrollChange, + sendByCtrlEnter, + onSendByCtrlEnterChange, isMobile }) => { const [localIsOpen, setLocalIsOpen] = useState(isOpen); @@ -142,6 +145,27 @@ const QuickSettingsPanel = ({ + {/* Input Settings */} +
+

Input Settings

+ + +

+ When enabled, pressing Ctrl+Enter will send the message instead of just Enter. This is useful for IME users to avoid accidental sends. +

+
+ {/* Whisper Dictation Settings - HIDDEN */}

Whisper Dictation