feat: use Ctrl/Cmd + ↑/↓ to navigate message history (#1501)
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import { getApiUrl } from '../config';
|
import { getApiUrl } from '../config';
|
||||||
import { generateSessionId } from '../sessions';
|
import { generateSessionId } from '../sessions';
|
||||||
import BottomMenu from './BottomMenu';
|
import BottomMenu from './BottomMenu';
|
||||||
@@ -226,6 +226,20 @@ export default function ChatView({
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const commandHistory = useMemo(() => {
|
||||||
|
return filteredMessages
|
||||||
|
.reduce<string[]>((history, message) => {
|
||||||
|
if (isUserMessage(message)) {
|
||||||
|
const text = message.content.find((c) => c.type === 'text')?.text?.trim();
|
||||||
|
if (text) {
|
||||||
|
history.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return history;
|
||||||
|
}, [])
|
||||||
|
.reverse();
|
||||||
|
}, [filteredMessages, isUserMessage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-screen items-center justify-center">
|
<div className="flex flex-col w-full h-screen items-center justify-center">
|
||||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle border-b border-borderSubtle">
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle border-b border-borderSubtle">
|
||||||
@@ -278,7 +292,12 @@ export default function ChatView({
|
|||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{isLoading && <LoadingGoose />}
|
{isLoading && <LoadingGoose />}
|
||||||
<Input handleSubmit={handleSubmit} isLoading={isLoading} onStop={onStopGoose} />
|
<Input
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onStop={onStopGoose}
|
||||||
|
commandHistory={commandHistory}
|
||||||
|
/>
|
||||||
<BottomMenu hasMessages={hasMessages} setView={setView} />
|
<BottomMenu hasMessages={hasMessages} setView={setView} />
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -7,12 +7,20 @@ interface InputProps {
|
|||||||
handleSubmit: (e: React.FormEvent) => void;
|
handleSubmit: (e: React.FormEvent) => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onStop?: () => void;
|
onStop?: () => void;
|
||||||
|
commandHistory?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Input({ handleSubmit, isLoading = false, onStop }: InputProps) {
|
export default function Input({
|
||||||
|
handleSubmit,
|
||||||
|
isLoading = false,
|
||||||
|
onStop,
|
||||||
|
commandHistory = [],
|
||||||
|
}: InputProps) {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
// State to track if the IME is composing (i.e., in the middle of Japanese IME input)
|
// State to track if the IME is composing (i.e., in the middle of Japanese IME input)
|
||||||
const [isComposing, setIsComposing] = useState(false);
|
const [isComposing, setIsComposing] = useState(false);
|
||||||
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||||
|
const [savedInput, setSavedInput] = useState('');
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,7 +58,45 @@ export default function Input({ handleSubmit, isLoading = false, onStop }: Input
|
|||||||
setIsComposing(false);
|
setIsComposing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleHistoryNavigation = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
// Save current input if we're just starting to navigate history
|
||||||
|
if (historyIndex === -1) {
|
||||||
|
setSavedInput(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new history index
|
||||||
|
let newIndex = historyIndex;
|
||||||
|
if (evt.key === 'ArrowUp') {
|
||||||
|
// Move backwards through history
|
||||||
|
if (historyIndex < commandHistory.length - 1) {
|
||||||
|
newIndex = historyIndex + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Move forwards through history
|
||||||
|
if (historyIndex > -1) {
|
||||||
|
newIndex = historyIndex - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update index and value
|
||||||
|
setHistoryIndex(newIndex);
|
||||||
|
if (newIndex === -1) {
|
||||||
|
// Restore saved input when going past the end of history
|
||||||
|
setValue(savedInput);
|
||||||
|
} else {
|
||||||
|
setValue(commandHistory[newIndex] || '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (evt: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
// Handle command history navigation
|
||||||
|
if ((evt.metaKey || evt.ctrlKey) && (evt.key === 'ArrowUp' || evt.key === 'ArrowDown')) {
|
||||||
|
handleHistoryNavigation(evt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (evt.key === 'Enter') {
|
if (evt.key === 'Enter') {
|
||||||
// should not trigger submit on Enter if it's composing (IME input in progress) or shift is pressed
|
// should not trigger submit on Enter if it's composing (IME input in progress) or shift is pressed
|
||||||
if (evt.shiftKey || isComposing) {
|
if (evt.shiftKey || isComposing) {
|
||||||
@@ -66,6 +112,8 @@ export default function Input({ handleSubmit, isLoading = false, onStop }: Input
|
|||||||
if (!isLoading && value.trim()) {
|
if (!isLoading && value.trim()) {
|
||||||
handleSubmit(new CustomEvent('submit', { detail: { value } }));
|
handleSubmit(new CustomEvent('submit', { detail: { value } }));
|
||||||
setValue('');
|
setValue('');
|
||||||
|
setHistoryIndex(-1);
|
||||||
|
setSavedInput('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -75,6 +123,8 @@ export default function Input({ handleSubmit, isLoading = false, onStop }: Input
|
|||||||
if (value.trim() && !isLoading) {
|
if (value.trim() && !isLoading) {
|
||||||
handleSubmit(new CustomEvent('submit', { detail: { value } }));
|
handleSubmit(new CustomEvent('submit', { detail: { value } }));
|
||||||
setValue('');
|
setValue('');
|
||||||
|
setHistoryIndex(-1);
|
||||||
|
setSavedInput('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,7 +144,7 @@ export default function Input({ handleSubmit, isLoading = false, onStop }: Input
|
|||||||
<textarea
|
<textarea
|
||||||
autoFocus
|
autoFocus
|
||||||
id="dynamic-textarea"
|
id="dynamic-textarea"
|
||||||
placeholder="What can goose help with?"
|
placeholder="What can goose help with? (Cmd/Ctrl + ↑/↓ for history)"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onCompositionStart={handleCompositionStart}
|
onCompositionStart={handleCompositionStart}
|
||||||
|
|||||||
Reference in New Issue
Block a user