mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-15 13:39:39 +00:00
Merge branch 'main' into fix/react-errors-and-localStorage-quota
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { userDb } from '../database/db.js';
|
import { userDb, db } from '../database/db.js';
|
||||||
import { generateToken, authenticateToken } from '../middleware/auth.js';
|
import { generateToken, authenticateToken } from '../middleware/auth.js';
|
||||||
|
|
||||||
const router = express.Router();
|
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' });
|
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)
|
// Use a transaction to prevent race conditions
|
||||||
const hasUsers = userDb.hasUsers();
|
db.prepare('BEGIN').run();
|
||||||
if (hasUsers) {
|
try {
|
||||||
return res.status(403).json({ error: 'User already exists. This is a single-user system.' });
|
// 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) {
|
} catch (error) {
|
||||||
console.error('Registration error:', error);
|
console.error('Registration error:', error);
|
||||||
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||||
|
|||||||
10
src/App.jsx
10
src/App.jsx
@@ -64,6 +64,10 @@ function AppContent() {
|
|||||||
const saved = localStorage.getItem('autoScrollToBottom');
|
const saved = localStorage.getItem('autoScrollToBottom');
|
||||||
return saved !== null ? JSON.parse(saved) : true;
|
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
|
// Session Protection System: Track sessions with active conversations to prevent
|
||||||
// automatic project updates from interrupting ongoing chats. When a user sends
|
// automatic project updates from interrupting ongoing chats. When a user sends
|
||||||
// a message, the session is marked as "active" and project updates are paused
|
// a message, the session is marked as "active" and project updates are paused
|
||||||
@@ -586,6 +590,7 @@ function AppContent() {
|
|||||||
autoExpandTools={autoExpandTools}
|
autoExpandTools={autoExpandTools}
|
||||||
showRawParameters={showRawParameters}
|
showRawParameters={showRawParameters}
|
||||||
autoScrollToBottom={autoScrollToBottom}
|
autoScrollToBottom={autoScrollToBottom}
|
||||||
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -617,6 +622,11 @@ function AppContent() {
|
|||||||
setAutoScrollToBottom(value);
|
setAutoScrollToBottom(value);
|
||||||
localStorage.setItem('autoScrollToBottom', JSON.stringify(value));
|
localStorage.setItem('autoScrollToBottom', JSON.stringify(value));
|
||||||
}}
|
}}
|
||||||
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
|
onSendByCtrlEnterChange={(value) => {
|
||||||
|
setSendByCtrlEnter(value);
|
||||||
|
localStorage.setItem('sendByCtrlEnter', JSON.stringify(value));
|
||||||
|
}}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1098,7 +1098,7 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => {
|
|||||||
// - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID
|
// - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID
|
||||||
//
|
//
|
||||||
// This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations.
|
// 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(() => {
|
const [input, setInput] = useState(() => {
|
||||||
if (typeof window !== 'undefined' && selectedProject) {
|
if (typeof window !== 'undefined' && selectedProject) {
|
||||||
return safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
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
|
toolResult: null // Will be updated when result comes in
|
||||||
}]);
|
}]);
|
||||||
} else if (part.type === 'text' && part.text?.trim()) {
|
} 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
|
// Add regular text message
|
||||||
setChatMessages(prev => [...prev, {
|
setChatMessages(prev => [...prev, {
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
content: part.text,
|
content: content,
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof messageData.content === 'string' && messageData.content.trim()) {
|
} 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
|
// Add regular text message
|
||||||
setChatMessages(prev => [...prev, {
|
setChatMessages(prev => [...prev, {
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
content: messageData.content,
|
content: content,
|
||||||
timestamp: new Date()
|
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
|
// Handle Enter key: Ctrl+Enter (Cmd+Enter on Mac) sends, Shift+Enter creates new line
|
||||||
if (e.key === 'Enter') {
|
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) {
|
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
// Ctrl+Enter or Cmd+Enter: Send message
|
// Ctrl+Enter or Cmd+Enter: Send message
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit(e);
|
handleSubmit(e);
|
||||||
} else if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
} else if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||||
// Plain Enter: Also send message (keeping original behavior)
|
// Plain Enter: Send message only if not in IME composition
|
||||||
e.preventDefault();
|
if (!sendByCtrlEnter) {
|
||||||
handleSubmit(e);
|
e.preventDefault();
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Shift+Enter: Allow default behavior (new line)
|
// Shift+Enter: Allow default behavior (new line)
|
||||||
}
|
}
|
||||||
@@ -2537,12 +2570,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
</div>
|
</div>
|
||||||
{/* Hint text */}
|
{/* Hint text */}
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center mt-2 hidden sm:block">
|
<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>
|
||||||
<div className={`text-xs text-gray-500 dark:text-gray-400 text-center mt-2 sm:hidden transition-opacity duration-200 ${
|
<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'
|
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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ function MainContent({
|
|||||||
onShowSettings, // Show tools settings panel
|
onShowSettings, // Show tools settings panel
|
||||||
autoExpandTools, // Auto-expand tool accordions
|
autoExpandTools, // Auto-expand tool accordions
|
||||||
showRawParameters, // Show raw parameters in 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);
|
const [editingFile, setEditingFile] = useState(null);
|
||||||
|
|
||||||
@@ -287,6 +288,7 @@ function MainContent({
|
|||||||
autoExpandTools={autoExpandTools}
|
autoExpandTools={autoExpandTools}
|
||||||
showRawParameters={showRawParameters}
|
showRawParameters={showRawParameters}
|
||||||
autoScrollToBottom={autoScrollToBottom}
|
autoScrollToBottom={autoScrollToBottom}
|
||||||
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
Mic,
|
Mic,
|
||||||
Brain,
|
Brain,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
FileText
|
FileText,
|
||||||
|
Languages
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import DarkModeToggle from './DarkModeToggle';
|
import DarkModeToggle from './DarkModeToggle';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
@@ -25,6 +26,8 @@ const QuickSettingsPanel = ({
|
|||||||
onShowRawParametersChange,
|
onShowRawParametersChange,
|
||||||
autoScrollToBottom,
|
autoScrollToBottom,
|
||||||
onAutoScrollChange,
|
onAutoScrollChange,
|
||||||
|
sendByCtrlEnter,
|
||||||
|
onSendByCtrlEnterChange,
|
||||||
isMobile
|
isMobile
|
||||||
}) => {
|
}) => {
|
||||||
const [localIsOpen, setLocalIsOpen] = useState(isOpen);
|
const [localIsOpen, setLocalIsOpen] = useState(isOpen);
|
||||||
@@ -142,6 +145,27 @@ const QuickSettingsPanel = ({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</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 */}
|
{/* Whisper Dictation Settings - HIDDEN */}
|
||||||
<div className="space-y-2" style={{ display: 'none' }}>
|
<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>
|
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>
|
||||||
|
|||||||
Reference in New Issue
Block a user