Merge branch 'main' into fix/react-errors-and-localStorage-quota

This commit is contained in:
viper151
2025-07-23 14:22:33 +02:00
committed by GitHub
5 changed files with 117 additions and 34 deletions

View File

@@ -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') {

View File

@@ -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}
/>
</div>
@@ -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}
/>
)}

View File

@@ -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
</div>
{/* Hint text */}
<div className="text-xs text-gray-500 dark:text-gray-400 text-center mt-2 hidden sm:block">
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"}
</div>
<div className={`text-xs text-gray-500 dark:text-gray-400 text-center mt-2 sm:hidden transition-opacity duration-200 ${
isInputFocused ? 'opacity-100' : 'opacity-0'
}`}>
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"}
</div>
</form>
</div>

View File

@@ -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}
/>
</ErrorBoundary>
</div>

View File

@@ -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 = ({
</label>
</div>
{/* Input Settings */}
<div className="space-y-2">
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Input Settings</h4>
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
<Languages className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Send by Ctrl+Enter
</span>
<input
type="checkbox"
checked={sendByCtrlEnter}
onChange={(e) => onSendByCtrlEnterChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
/>
</label>
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3">
When enabled, pressing Ctrl+Enter will send the message instead of just Enter. This is useful for IME users to avoid accidental sends.
</p>
</div>
{/* Whisper Dictation Settings - HIDDEN */}
<div className="space-y-2" style={{ display: 'none' }}>
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>