feat: auto-focus chat input on session switch and connection
This commit is contained in:
@@ -86,7 +86,7 @@ export default function App() {
|
|||||||
<Header status={status} sessionKey={activeSession} onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} activeSessionData={sessions.find(s => s.key === activeSession)} onLogout={logout} soundEnabled={soundEnabled} onToggleSound={toggleSound} />
|
<Header status={status} sessionKey={activeSession} onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} activeSessionData={sessions.find(s => s.key === activeSession)} onLogout={logout} soundEnabled={soundEnabled} onToggleSound={toggleSound} />
|
||||||
<ConnectionBanner status={status} />
|
<ConnectionBanner status={status} />
|
||||||
<Suspense fallback={<div className="flex-1 flex items-center justify-center text-zinc-500"><div className="animate-pulse text-sm">Loading…</div></div>}>
|
<Suspense fallback={<div className="flex-1 flex items-center justify-center text-zinc-500"><div className="animate-pulse text-sm">Loading…</div></div>}>
|
||||||
<Chat messages={messages} isGenerating={isGenerating} isLoadingHistory={isLoadingHistory} status={status} onSend={sendMessage} onAbort={abort} />
|
<Chat messages={messages} isGenerating={isGenerating} isLoadingHistory={isLoadingHistory} status={status} sessionKey={activeSession} onSend={sendMessage} onAbort={abort} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<KeyboardShortcuts open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
|
<KeyboardShortcuts open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface Props {
|
|||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
isLoadingHistory: boolean;
|
isLoadingHistory: boolean;
|
||||||
status: ConnectionStatus;
|
status: ConnectionStatus;
|
||||||
|
sessionKey?: string;
|
||||||
onSend: (text: string, attachments?: Array<{ mimeType: string; fileName: string; content: string }>) => void;
|
onSend: (text: string, attachments?: Array<{ mimeType: string; fileName: string; content: string }>) => void;
|
||||||
onAbort: () => void;
|
onAbort: () => void;
|
||||||
}
|
}
|
||||||
@@ -64,7 +65,7 @@ function getDateKey(ts: number): string {
|
|||||||
/** Threshold in pixels — if the user is within this distance of the bottom, auto-scroll */
|
/** Threshold in pixels — if the user is within this distance of the bottom, auto-scroll */
|
||||||
const SCROLL_THRESHOLD = 150;
|
const SCROLL_THRESHOLD = 150;
|
||||||
|
|
||||||
export function Chat({ messages, isGenerating, isLoadingHistory, status, onSend, onAbort }: Props) {
|
export function Chat({ messages, isGenerating, isLoadingHistory, status, sessionKey, onSend, onAbort }: Props) {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -176,7 +177,7 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, onSend,
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ChatInput onSend={handleSend} onAbort={onAbort} isGenerating={isGenerating} disabled={status !== 'connected'} />
|
<ChatInput onSend={handleSend} onAbort={onAbort} isGenerating={isGenerating} disabled={status !== 'connected'} sessionKey={sessionKey} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface Props {
|
|||||||
onAbort: () => void;
|
onAbort: () => void;
|
||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
sessionKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_BASE64_CHARS = 300 * 1024; // ~225KB real, well under 512KB WS limit (JSON overhead + base64 bloat)
|
const MAX_BASE64_CHARS = 300 * 1024; // ~225KB real, well under 512KB WS limit (JSON overhead + base64 bloat)
|
||||||
@@ -79,7 +80,7 @@ function formatSize(bytes: number): string {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({ onSend, onAbort, isGenerating, disabled }: Props) {
|
export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey }: Props) {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
const [files, setFiles] = useState<FileAttachment[]>([]);
|
const [files, setFiles] = useState<FileAttachment[]>([]);
|
||||||
@@ -94,6 +95,15 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled }: Props) {
|
|||||||
}
|
}
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
|
// Auto-focus textarea when session changes or connection becomes active
|
||||||
|
useEffect(() => {
|
||||||
|
if (!disabled && textareaRef.current) {
|
||||||
|
// Small delay to let the DOM settle after session switch
|
||||||
|
const timer = setTimeout(() => textareaRef.current?.focus(), 50);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [sessionKey, disabled]);
|
||||||
|
|
||||||
const addFiles = useCallback(async (fileList: FileList | File[]) => {
|
const addFiles = useCallback(async (fileList: FileList | File[]) => {
|
||||||
const newFiles: FileAttachment[] = [];
|
const newFiles: FileAttachment[] = [];
|
||||||
for (const file of Array.from(fileList)) {
|
for (const file of Array.from(fileList)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user