Skip to content

Commit 0bf6686

Browse files
authored
feat: add keyboard-only operation to history list (#385)
* feat: add keyboard-only operation to history list * docs: update changelog
1 parent 9f04fb1 commit 0bf6686

10 files changed

Lines changed: 175 additions & 78 deletions

File tree

docs/content.en/docs/release-notes/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Information about release notes of Coco Server is provided here.
2222
- feat: service list popup box supports keyboard-only operation #359
2323
- feat: networked search data sources support search and keyboard-only operation #367
2424
- feat: add application management to the plugin #374
25+
- feat: add keyboard-only operation to history list #385
2526

2627
### Bug fix
2728

src/components/Assistant/ChatHeader.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { useConnectStore } from "@/stores/connectStore";
3333
import platformAdapter from "@/utils/platformAdapter";
3434
import VisibleKey from "../Common/VisibleKey";
3535
import { useShortcutsStore } from "@/stores/shortcutsStore";
36+
import { HISTORY_PANEL_ID } from "@/constants";
3637

3738
interface ChatHeaderProps {
3839
onCreateNewChat: () => void;
@@ -49,6 +50,7 @@ interface ChatHeaderProps {
4950
export function ChatHeader({
5051
onCreateNewChat,
5152
onOpenChatAI,
53+
isSidebarOpen,
5254
setIsSidebarOpen,
5355
activeChat,
5456
reconnect,
@@ -206,11 +208,12 @@ export function ChatHeader({
206208
e.stopPropagation();
207209
setIsSidebarOpen();
208210
}}
209-
className="p-2 py-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
211+
aria-controls={isSidebarOpen ? HISTORY_PANEL_ID : void 0}
212+
className="py-1 px-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
210213
>
211214
<VisibleKey
212215
shortcut={historicalRecords}
213-
onKeypress={setIsSidebarOpen}
216+
onKeyPress={setIsSidebarOpen}
214217
>
215218
<HistoryIcon className="h-4 w-4" />
216219
</VisibleKey>
@@ -255,7 +258,7 @@ export function ChatHeader({
255258
onClick={onCreateNewChat}
256259
className="p-2 py-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
257260
>
258-
<VisibleKey shortcut={newSession} onKeypress={onCreateNewChat}>
261+
<VisibleKey shortcut={newSession} onKeyPress={onCreateNewChat}>
259262
<MessageSquarePlus className="h-4 w-4 relative top-0.5" />
260263
</VisibleKey>
261264
</button>
@@ -278,7 +281,7 @@ export function ChatHeader({
278281
"text-blue-500": isPinned,
279282
})}
280283
>
281-
<VisibleKey shortcut={fixedWindow} onKeypress={togglePin}>
284+
<VisibleKey shortcut={fixedWindow} onKeyPress={togglePin}>
282285
{isPinned ? <PinIcon /> : <PinOffIcon />}
283286
</VisibleKey>
284287
</button>
@@ -290,7 +293,7 @@ export function ChatHeader({
290293
>
291294
<VisibleKey
292295
shortcut={serviceList}
293-
onKeypress={() => {
296+
onKeyPress={() => {
294297
serverListButtonRef.current?.click();
295298
}}
296299
>
@@ -318,7 +321,7 @@ export function ChatHeader({
318321
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
319322
disabled={isRefreshing}
320323
>
321-
<VisibleKey shortcut="R" onKeypress={handleRefresh}>
324+
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
322325
<RefreshCw
323326
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
324327
isRefreshing ? "animate-spin" : ""
@@ -365,7 +368,10 @@ export function ChatHeader({
365368
/>
366369
<div className="size-4 flex justify-end">
367370
{currentService?.id === server.id && (
368-
<VisibleKey shortcut="↓↑" className="min-w-6">
371+
<VisibleKey
372+
shortcut="↓↑"
373+
shortcutClassName="w-6 -translate-x-4"
374+
>
369375
<Check className="w-full h-full text-gray-500 dark:text-gray-400" />
370376
</VisibleKey>
371377
)}
@@ -394,7 +400,7 @@ export function ChatHeader({
394400

395401
{isChatPage ? null : (
396402
<button className="inline-flex" onClick={onOpenChatAI}>
397-
<VisibleKey shortcut={external} onKeypress={onOpenChatAI}>
403+
<VisibleKey shortcut={external} onKeyPress={onOpenChatAI}>
398404
<WindowsFullIcon className="rotate-30 scale-x-[-1]" />
399405
</VisibleKey>
400406
</button>

src/components/Assistant/ChatSidebar.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,17 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
3939
overflow-hidden
4040
`}
4141
>
42-
<HistoryList
43-
list={chats}
44-
active={activeChat}
45-
onSearch={onSearch}
46-
onRefresh={fetchChatHistory}
47-
onSelect={onSelectChat}
48-
onRename={onRename}
49-
onRemove={onDeleteChat}
50-
/>
42+
{isSidebarOpen && (
43+
<HistoryList
44+
list={chats}
45+
active={activeChat}
46+
onSearch={onSearch}
47+
onRefresh={fetchChatHistory}
48+
onSelect={onSelectChat}
49+
onRename={onRename}
50+
onRemove={onDeleteChat}
51+
/>
52+
)}
5153
{/* <Sidebar
5254
chats={chats}
5355
activeChat={activeChat}

src/components/AudioRecording/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
163163
}
164164
)}
165165
>
166-
<VisibleKey shortcut={voiceInput} onKeypress={startRecording}>
166+
<VisibleKey shortcut={voiceInput} onKeyPress={startRecording}>
167167
<Mic className="size-4 text-[#999]" onClick={startRecording} />
168168
</VisibleKey>
169169
</div>

src/components/Common/HistoryList/index.tsx

Lines changed: 112 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ import {
55
DialogPanel,
66
DialogTitle,
77
Input,
8-
Menu,
9-
MenuButton,
10-
MenuItem,
11-
MenuItems,
8+
Popover,
9+
PopoverButton,
10+
PopoverPanel,
1211
} from "@headlessui/react";
1312
import { debounce, groupBy, isNil } from "lodash-es";
14-
import { FC, useMemo, useState } from "react";
13+
import { FC, useEffect, useMemo, useRef, useState } from "react";
1514
import dayjs from "dayjs";
1615
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
1716
import clsx from "clsx";
1817
import { Ellipsis, Pencil, RefreshCcw, Search, Trash2 } from "lucide-react";
1918
import { useTranslation } from "react-i18next";
19+
import VisibleKey from "../VisibleKey";
20+
import { HISTORY_PANEL_ID } from "@/constants";
21+
import { useKeyPress } from "ahooks";
2022

2123
dayjs.extend(isSameOrAfter);
2224

@@ -36,6 +38,9 @@ const HistoryList: FC<HistoryListProps> = (props) => {
3638
const { t } = useTranslation();
3739
const [isEdit, setIsEdit] = useState(false);
3840
const [isOpen, setIsOpen] = useState(false);
41+
const listRef = useRef<HTMLDivElement>(null);
42+
const searchInputRef = useRef<HTMLInputElement>(null);
43+
const moreButtonRef = useRef<HTMLButtonElement>(null);
3944

4045
const sortedList = useMemo(() => {
4146
if (isNil(list)) return {};
@@ -74,13 +79,15 @@ const HistoryList: FC<HistoryListProps> = (props) => {
7479
{
7580
label: "history_list.menu.rename",
7681
icon: Pencil,
82+
shortcut: "R",
7783
onClick: () => {
7884
setIsEdit(true);
7985
},
8086
},
8187
{
8288
label: "history_list.menu.delete",
8389
icon: Trash2,
90+
shortcut: "D",
8491
iconColor: "#FF2018",
8592
onClick: () => {
8693
setIsOpen(true);
@@ -92,17 +99,54 @@ const HistoryList: FC<HistoryListProps> = (props) => {
9299
return debounce((value: string) => onSearch(value), 500);
93100
}, [onSearch]);
94101

102+
useKeyPress(["uparrow", "downarrow"], (_, key) => {
103+
const index = list.findIndex((item) => item._id === active?._id);
104+
const length = list.length;
105+
106+
let nextIndex = index;
107+
108+
switch (key) {
109+
case "uparrow":
110+
nextIndex = index === 0 ? length - 1 : index - 1;
111+
break;
112+
case "downarrow":
113+
nextIndex = index === length - 1 ? 0 : index + 1;
114+
break;
115+
}
116+
117+
onSelect(list[nextIndex]);
118+
});
119+
120+
useEffect(() => {
121+
if (!active?._id || !listRef.current) return;
122+
123+
const activeEl = listRef.current.querySelector(`#${active._id}`);
124+
125+
activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
126+
}, [active?._id]);
127+
95128
return (
96129
<div
130+
ref={listRef}
131+
id={HISTORY_PANEL_ID}
97132
className={clsx(
98133
"h-full overflow-auto px-3 py-2 text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
99134
)}
100135
>
101136
<div className="flex gap-1 children:h-8">
102137
<div className="flex-1 flex items-center gap-2 px-2 rounded-lg border transition border-[#E6E6E6] bg-[#F8F9FA] dark:bg-[#2B3444] dark:border-[#343D4D] focus-within:border-[#0061FF]">
103-
<Search className="size-4 text-[#6B7280]" />
138+
<VisibleKey
139+
shortcut="I"
140+
onKeyPress={() => {
141+
searchInputRef.current?.focus();
142+
}}
143+
>
144+
<Search className="size-4 text-[#6B7280]" />
145+
</VisibleKey>
104146

105147
<Input
148+
autoFocus
149+
ref={searchInputRef}
106150
className="w-full bg-transparent outline-none"
107151
placeholder={t("history_list.search.placeholder")}
108152
onChange={(event) => {
@@ -115,7 +159,9 @@ const HistoryList: FC<HistoryListProps> = (props) => {
115159
className="size-8 flex items-center justify-center rounded-lg border text-[#0072FF] border-[#E6E6E6] bg-[#F3F4F6] dark:border-[#343D4D] dark:bg-[#1F2937] hover:bg-[#F8F9FA] dark:hover:bg-[#353F4D] cursor-pointer transition"
116160
onClick={onRefresh}
117161
>
118-
<RefreshCcw className="size-4" />
162+
<VisibleKey shortcut="R" onKeyPress={onRefresh}>
163+
<RefreshCcw className="size-4" />
164+
</VisibleKey>
119165
</div>
120166
</div>
121167

@@ -135,6 +181,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
135181
return (
136182
<li
137183
key={_id}
184+
id={_id}
138185
className={clsx(
139186
"flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
140187
{
@@ -178,48 +225,73 @@ const HistoryList: FC<HistoryListProps> = (props) => {
178225
<span className="truncate">{title}</span>
179226
)}
180227

181-
<Menu>
228+
<div className="flex items-center gap-2">
182229
{isActive && !isEdit && (
183-
<MenuButton>
184-
<Ellipsis className="size-4 text-[#979797]" />
185-
</MenuButton>
230+
<VisibleKey
231+
shortcut="↑↓"
232+
rootClassName="w-6"
233+
shortcutClassName="w-6"
234+
/>
186235
)}
187236

188-
<MenuItems
189-
anchor="bottom"
190-
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
191-
onClick={(event) => {
192-
event.stopPropagation();
193-
}}
194-
>
195-
{menuItems.map((menuItem) => {
196-
const {
197-
label,
198-
icon: Icon,
199-
iconColor,
200-
onClick,
201-
} = menuItem;
202-
203-
return (
204-
<MenuItem key={label}>
237+
<Popover>
238+
{isActive && !isEdit && (
239+
<PopoverButton
240+
ref={moreButtonRef}
241+
className="flex gap-2"
242+
>
243+
<VisibleKey
244+
shortcut="O"
245+
onKeyPress={() => {
246+
moreButtonRef.current?.click();
247+
}}
248+
>
249+
<Ellipsis className="size-4 text-[#979797]" />
250+
</VisibleKey>
251+
</PopoverButton>
252+
)}
253+
254+
<PopoverPanel
255+
anchor="bottom"
256+
className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
257+
onClick={(event) => {
258+
event.stopPropagation();
259+
}}
260+
>
261+
{menuItems.map((menuItem) => {
262+
const {
263+
label,
264+
icon: Icon,
265+
shortcut,
266+
iconColor,
267+
onClick,
268+
} = menuItem;
269+
270+
return (
205271
<button
272+
key={label}
206273
className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
207-
onClick={() => onClick()}
274+
onClick={onClick}
208275
>
209-
<Icon
210-
className="size-4"
211-
style={{
212-
color: iconColor,
213-
}}
214-
/>
276+
<VisibleKey
277+
shortcut={shortcut}
278+
onKeyPress={onClick}
279+
>
280+
<Icon
281+
className="size-4"
282+
style={{
283+
color: iconColor,
284+
}}
285+
/>
286+
</VisibleKey>
215287

216288
<span>{t(label)}</span>
217289
</button>
218-
</MenuItem>
219-
);
220-
})}
221-
</MenuItems>
222-
</Menu>
290+
);
291+
})}
292+
</PopoverPanel>
293+
</Popover>
294+
</div>
223295
</div>
224296
</li>
225297
);

src/components/Common/UI/Footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default function Footer({
8989
"pl-2": updateInfo?.available,
9090
})}
9191
>
92-
<VisibleKey shortcut={fixedWindow} onKeypress={togglePin}>
92+
<VisibleKey shortcut={fixedWindow} onKeyPress={togglePin}>
9393
{isPinned ? <PinIcon /> : <PinOffIcon />}
9494
</VisibleKey>
9595
</button>

0 commit comments

Comments
 (0)