Files
claude-web-ui/frontend/src/components/ChatPanel.jsx
Nikolas Syring 1186cb1b5e feat: Add OIDC authentication with Authentik integration
- Add OIDC login flow with Authentik provider
- Implement session-based auth with Redis store
- Add avatar display from OIDC claims
- Fix input field performance with react-textarea-autosize
- Stabilize callbacks to prevent unnecessary re-renders
- Fix history loading to skip empty session files
- Add 2-row default height for input textarea

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 06:07:22 +01:00

220 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { memo, useCallback, useMemo, useRef } from 'react';
import { MessageList } from './MessageList';
import { ChatInput } from './ChatInput';
import { StatusBar } from './StatusBar';
import { PermissionDialog } from './PermissionDialog';
import { useSessionManager } from '../contexts/SessionContext';
import { Bot } from 'lucide-react';
// Wrapper to compute placeholder inside and prevent parent re-renders from affecting input
const MemoizedChatInput = memo(function MemoizedChatInput({
onSend, onStop, disabled, isProcessing, sessionId, connected, active
}) {
const placeholder = !connected
? 'Connecting...'
: !active
? 'Start session to begin'
: 'Type your message...';
return (
<ChatInput
onSend={onSend}
onStop={onStop}
disabled={disabled}
isProcessing={isProcessing}
sessionId={sessionId}
placeholder={placeholder}
/>
);
}, (prevProps, nextProps) => {
// Custom comparison - only re-render if these specific props change
return (
prevProps.disabled === nextProps.disabled &&
prevProps.isProcessing === nextProps.isProcessing &&
prevProps.sessionId === nextProps.sessionId &&
prevProps.connected === nextProps.connected &&
prevProps.active === nextProps.active &&
prevProps.onSend === nextProps.onSend &&
prevProps.onStop === nextProps.onStop
);
});
// Welcome screen when no messages
const WelcomeScreen = memo(function WelcomeScreen({ session, onStart }) {
const hostName = session.host.charAt(0).toUpperCase() + session.host.slice(1);
const projectName = session.project.split('/').pop() || session.project;
return (
<div className="flex-1 min-h-0 flex items-center justify-center p-8">
<div className="text-center text-dark-500 max-w-md">
<Bot className="w-16 h-16 mx-auto mb-4 opacity-50" />
<h2 className="text-xl font-semibold mb-2 text-dark-300">
{hostName}: {projectName}
</h2>
<p className="text-sm mb-6">
{session.active
? 'Session is active. Start chatting with Claude.'
: 'Click "Start Session" in the sidebar or press the button below to begin.'}
</p>
{!session.active && (
<button
onClick={onStart}
disabled={!session.connected}
className={`
px-6 py-3 rounded-lg font-medium transition-colors
${session.connected
? 'bg-green-600 hover:bg-green-500 text-white'
: 'bg-dark-700 text-dark-500 cursor-not-allowed'}
`}
>
{session.connected ? 'Start Session' : 'Connecting...'}
</button>
)}
</div>
</div>
);
});
// Error banner
const ErrorBanner = memo(function ErrorBanner({ error, onClear }) {
if (!error) return null;
return (
<div className="bg-red-900/50 border-b border-red-800 px-4 py-2 flex justify-between items-center flex-shrink-0">
<span className="text-red-200 text-sm">{error}</span>
<button
onClick={onClear}
className="text-red-400 hover:text-red-300 text-lg leading-none"
>
×
</button>
</div>
);
});
// Use a separate hook that memoizes everything to prevent unnecessary re-renders
function useMemoizedSession(sessionId) {
const {
sessions,
sessionMessages,
startClaudeSession,
stopClaudeSession,
sendMessage,
stopGeneration,
clearMessages,
changePermissionMode,
respondToPermission,
} = useSessionManager();
const session = sessions[sessionId];
const messages = sessionMessages[sessionId] || [];
// Memoize the combined session object
const sessionWithMessages = useMemo(() => {
return session ? { ...session, messages } : null;
}, [session, messages]);
// Memoize all action functions - use individual functions as deps, not the whole manager
const actions = useMemo(() => ({
start: () => startClaudeSession(sessionId),
stop: () => stopClaudeSession(sessionId),
send: (msg, attachments) => sendMessage(sessionId, msg, attachments),
stopGeneration: () => stopGeneration(sessionId),
clearMessages: () => clearMessages(sessionId),
changePermissionMode: (mode) => changePermissionMode(sessionId, mode),
respondToPermission: (reqId, allow) => respondToPermission(sessionId, reqId, allow),
}), [sessionId, startClaudeSession, stopClaudeSession, sendMessage, stopGeneration, clearMessages, changePermissionMode, respondToPermission]);
return { session: sessionWithMessages, ...actions };
}
export const ChatPanel = memo(function ChatPanel({ sessionId }) {
const {
session,
start,
stop,
send,
stopGeneration,
clearMessages,
changePermissionMode,
respondToPermission,
} = useMemoizedSession(sessionId);
const handleClearError = useCallback(() => {
// We'd need to add this to the session context
// For now, errors auto-clear on next action
}, []);
// Use refs for callbacks to keep them stable across re-renders
const sendRef = useRef(send);
sendRef.current = send;
const stopGenerationRef = useRef(stopGeneration);
stopGenerationRef.current = stopGeneration;
// These callbacks never change identity, preventing ChatInput re-renders
const handleSendMessage = useCallback((message, attachments = []) => {
if (message.trim() || attachments.length > 0) {
sendRef.current(message, attachments);
}
}, []);
const handleStopGeneration = useCallback(() => {
stopGenerationRef.current();
}, []);
if (!session) {
return (
<div className="flex-1 flex items-center justify-center text-dark-500">
Session not found
</div>
);
}
const hasMessages = session.messages && session.messages.length > 0;
return (
<div className="flex flex-col h-full bg-dark-950">
{/* Error Banner */}
<ErrorBanner error={session.error} onClear={handleClearError} />
{/* Messages or Welcome */}
{hasMessages || session.active ? (
<MessageList
messages={session.messages || []}
isProcessing={session.isProcessing}
/>
) : (
<WelcomeScreen session={session} onStart={start} />
)}
{/* Status Bar */}
<StatusBar
sessionStats={session.stats}
isProcessing={session.isProcessing}
connected={session.connected}
permissionMode={session.permissionMode}
controlInitialized={session.controlInitialized}
onChangeMode={(mode) => changePermissionMode(mode)}
/>
{/* Input - memoized props to prevent re-renders during streaming */}
<MemoizedChatInput
onSend={handleSendMessage}
onStop={handleStopGeneration}
disabled={!session.active}
isProcessing={session.isProcessing}
sessionId={session.claudeSessionId}
connected={session.connected}
active={session.active}
/>
{/* Permission Dialog */}
<PermissionDialog
permission={session.pendingPermission}
onAllow={(requestId) => respondToPermission(requestId, true)}
onDeny={(requestId) => respondToPermission(requestId, false)}
/>
</div>
);
});