Skip to content

Commit 1cace28

Browse files
authored
feat: add shortcuts for some icon buttons (#334)
* feat: add shortcuts for some icon buttons * feat: support for switching the fixed state of the window * refactor: optimize the issue of page jumping caused by the display of shortcut keys * feat: deep thinking and networking search add shortcuts * refactor: changing the default shortcut keys * refactor: hide the voice input function button * docs: update changelog
1 parent eb32b03 commit 1cace28

12 files changed

Lines changed: 410 additions & 182 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Information about release notes of Coco Server is provided here.
1515

1616
- Linux support for application search #330
1717

18+
- Add shortcuts to most icon buttons #334
19+
1820
### Bug fix
1921

2022
### Improvements

src/components/Assistant/ChatHeader.tsx

Lines changed: 148 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
Check,
77
Server,
88
} from "lucide-react";
9-
import { useState, useEffect, useCallback } from "react";
9+
import { useState, useEffect, useCallback, useRef } from "react";
1010
import {
1111
Menu,
1212
MenuButton,
@@ -30,6 +30,10 @@ import type { Chat } from "./types";
3030
import { useConnectStore } from "@/stores/connectStore";
3131
import platformAdapter from "@/utils/platformAdapter";
3232
import { list_coco_servers } from "@/commands";
33+
import VisibleKey from "../Common/VisibleKey";
34+
import { useShortcutsStore } from "@/stores/shortcutsStore";
35+
import { useBoolean } from "ahooks";
36+
import clsx from "clsx";
3337

3438
interface ChatHeaderProps {
3539
onCreateNewChat: () => void;
@@ -68,6 +72,18 @@ export function ChatHeader({
6872
const setCurrentService = useConnectStore((state) => state.setCurrentService);
6973

7074
const isTauri = useAppStore((state) => state.isTauri);
75+
const historicalRecords = useShortcutsStore((state) => {
76+
return state.historicalRecords;
77+
});
78+
const newSession = useShortcutsStore((state) => {
79+
return state.newSession;
80+
});
81+
const fixedWindow = useShortcutsStore((state) => {
82+
return state.fixedWindow;
83+
});
84+
const serviceList = useShortcutsStore((state) => state.serviceList);
85+
const external = useShortcutsStore((state) => state.external);
86+
const serverListButtonRef = useRef<HTMLButtonElement>(null);
7187

7288
const fetchServers = useCallback(
7389
async (resetSelection: boolean) => {
@@ -163,9 +179,14 @@ export function ChatHeader({
163179
e.stopPropagation();
164180
setIsSidebarOpen();
165181
}}
166-
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
182+
className="inline-flex size-[34px] p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
167183
>
168-
<HistoryIcon />
184+
<VisibleKey
185+
shortcut={historicalRecords}
186+
onKeypress={setIsSidebarOpen}
187+
>
188+
<HistoryIcon />
189+
</VisibleKey>
169190
</button>
170191
)}
171192

@@ -207,7 +228,9 @@ export function ChatHeader({
207228
onClick={onCreateNewChat}
208229
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
209230
>
210-
<MessageSquarePlus className="h-4 w-4" />
231+
<VisibleKey shortcut={newSession} onKeypress={onCreateNewChat}>
232+
<MessageSquarePlus className="h-4 w-4" />
233+
</VisibleKey>
211234
</button>
212235
) : null}
213236
</div>
@@ -220,117 +243,137 @@ export function ChatHeader({
220243
</h2>
221244
</div>
222245

223-
{isTauri ? <div className="flex items-center gap-2">
224-
<button
225-
onClick={togglePin}
226-
className={`${isPinned ? "text-blue-500" : ""}`}
227-
>
228-
{isPinned ? <PinIcon /> : <PinOffIcon />}
229-
</button>
246+
{isTauri ? (
247+
<div className="flex items-center gap-2">
248+
<button
249+
onClick={togglePin}
250+
className={clsx("inline-flex", {
251+
"text-blue-500": isPinned,
252+
})}
253+
>
254+
<VisibleKey shortcut={fixedWindow} onKeypress={togglePin}>
255+
{isPinned ? <PinIcon /> : <PinOffIcon />}
256+
</VisibleKey>
257+
</button>
230258

231-
<Popover className="relative">
232-
<PopoverButton className="flex items-center">
233-
<ServerIcon />
234-
</PopoverButton>
259+
<Popover className="relative">
260+
<PopoverButton
261+
ref={serverListButtonRef}
262+
className="flex items-center"
263+
>
264+
<VisibleKey
265+
shortcut={serviceList}
266+
onKeypress={() => {
267+
serverListButtonRef.current?.click();
268+
}}
269+
>
270+
<ServerIcon />
271+
</VisibleKey>
272+
</PopoverButton>
235273

236-
<PopoverPanel className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
237-
<div className="p-3">
238-
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
239-
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
240-
Servers
241-
</h3>
242-
<div className="flex items-center gap-2">
243-
<button
244-
onClick={openSettings}
245-
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
246-
>
247-
<Settings className="h-4 w-4 text-[#0287FF]" />
248-
</button>
249-
<button
250-
onClick={async () => {
251-
setIsRefreshing(true);
252-
await fetchServers(false);
253-
setTimeout(() => setIsRefreshing(false), 1000);
254-
}}
255-
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
256-
disabled={isRefreshing}
257-
>
258-
<RefreshCw
259-
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
260-
isRefreshing ? "animate-spin" : ""
261-
}`}
262-
/>
263-
</button>
264-
</div>
265-
</div>
266-
<div className="space-y-1">
267-
{serverList.length > 0 ? (
268-
serverList.map((server) => (
269-
<div
270-
key={server.id}
271-
onClick={() => switchServer(server)}
272-
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap ${
273-
currentService?.id === server.id
274-
? "bg-gray-100 dark:bg-gray-800"
275-
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
276-
}`}
274+
<PopoverPanel className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
275+
<div className="p-3">
276+
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
277+
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
278+
Servers
279+
</h3>
280+
<div className="flex items-center gap-2">
281+
<button
282+
onClick={openSettings}
283+
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
277284
>
278-
<div className="flex items-center gap-2 overflow-hidden min-w-0">
279-
<img
280-
src={server?.provider?.icon || logoImg}
281-
alt={server.name}
282-
className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800"
283-
/>
284-
<div className="text-left flex-1 min-w-0">
285-
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
286-
{server.name}
287-
</div>
288-
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
289-
AI Assistant: {server.assistantCount || 1}
285+
<Settings className="h-4 w-4 text-[#0287FF]" />
286+
</button>
287+
<button
288+
onClick={async () => {
289+
setIsRefreshing(true);
290+
await fetchServers(false);
291+
setTimeout(() => setIsRefreshing(false), 1000);
292+
}}
293+
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
294+
disabled={isRefreshing}
295+
>
296+
<RefreshCw
297+
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
298+
isRefreshing ? "animate-spin" : ""
299+
}`}
300+
/>
301+
</button>
302+
</div>
303+
</div>
304+
<div className="space-y-1">
305+
{serverList.length > 0 ? (
306+
serverList.map((server) => (
307+
<div
308+
key={server.id}
309+
onClick={() => switchServer(server)}
310+
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap ${
311+
currentService?.id === server.id
312+
? "bg-gray-100 dark:bg-gray-800"
313+
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
314+
}`}
315+
>
316+
<div className="flex items-center gap-2 overflow-hidden min-w-0">
317+
<img
318+
src={server?.provider?.icon || logoImg}
319+
alt={server.name}
320+
className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800"
321+
/>
322+
<div className="text-left flex-1 min-w-0">
323+
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
324+
{server.name}
325+
</div>
326+
<div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
327+
AI Assistant: {server.assistantCount || 1}
328+
</div>
290329
</div>
291330
</div>
292-
</div>
293-
<div className="flex items-center gap-2">
294-
<span
295-
className={`w-3 h-3 rounded-full ${
296-
server.health?.status
297-
? `bg-[${server.health?.status}]`
298-
: "bg-gray-400 dark:bg-gray-600"
299-
}`}
300-
/>
301-
<div className="w-4 h-4">
302-
{currentService?.id === server.id && (
303-
<Check className="w-full h-full text-gray-500 dark:text-gray-400" />
304-
)}
331+
<div className="flex items-center gap-2">
332+
<span
333+
className={`w-3 h-3 rounded-full ${
334+
server.health?.status
335+
? `bg-[${server.health?.status}]`
336+
: "bg-gray-400 dark:bg-gray-600"
337+
}`}
338+
/>
339+
<div className="w-4 h-4">
340+
{currentService?.id === server.id && (
341+
<Check className="w-full h-full text-gray-500 dark:text-gray-400" />
342+
)}
343+
</div>
305344
</div>
306345
</div>
346+
))
347+
) : (
348+
<div className="flex flex-col items-center justify-center py-6 text-center">
349+
<Server className="w-8 h-8 text-gray-400 dark:text-gray-600 mb-2" />
350+
<p className="text-sm text-gray-500 dark:text-gray-400">
351+
{t("assistant.chat.noServers")}
352+
</p>
353+
<button
354+
onClick={openSettings}
355+
className="mt-2 text-xs text-[#0287FF] hover:underline"
356+
>
357+
{t("assistant.chat.addServer")}
358+
</button>
307359
</div>
308-
))
309-
) : (
310-
<div className="flex flex-col items-center justify-center py-6 text-center">
311-
<Server className="w-8 h-8 text-gray-400 dark:text-gray-600 mb-2" />
312-
<p className="text-sm text-gray-500 dark:text-gray-400">
313-
{t("assistant.chat.noServers")}
314-
</p>
315-
<button
316-
onClick={openSettings}
317-
className="mt-2 text-xs text-[#0287FF] hover:underline"
318-
>
319-
{t("assistant.chat.addServer")}
320-
</button>
321-
</div>
322-
)}
360+
)}
361+
</div>
323362
</div>
324-
</div>
325-
</PopoverPanel>
326-
</Popover>
363+
</PopoverPanel>
364+
</Popover>
327365

328-
{isChatPage ? null : (
329-
<button onClick={onOpenChatAI}>
330-
<WindowsFullIcon className="rotate-30 scale-x-[-1]" />
331-
</button>
332-
)}
333-
</div>: <div/>}
366+
{isChatPage ? null : (
367+
<button className="inline-flex" onClick={onOpenChatAI}>
368+
<VisibleKey shortcut={external} onKeypress={onOpenChatAI}>
369+
<WindowsFullIcon className="rotate-30 scale-x-[-1]" />
370+
</VisibleKey>
371+
</button>
372+
)}
373+
</div>
374+
) : (
375+
<div />
376+
)}
334377
</header>
335378
);
336379
}

src/components/AudioRecording/index.tsx

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import RecordPlugin from "wavesurfer.js/dist/plugins/record.esm.js";
1212
import { useConnectStore } from "@/stores/connectStore";
1313
import { useShortcutsStore } from "@/stores/shortcutsStore";
1414
import { transcription } from "@/commands";
15+
import VisibleKey from "../Common/VisibleKey";
1516

1617
interface AudioRecordingProps {
1718
onChange?: (text: string) => void;
@@ -40,12 +41,6 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
4041
const recordRef = useRef<RecordPlugin>();
4142
const withVisibility = useAppStore((state) => state.withVisibility);
4243
const currentService = useConnectStore((state) => state.currentService);
43-
const modifierKeyPressed = useShortcutsStore((state) => {
44-
return state.modifierKeyPressed;
45-
});
46-
const modifierKey = useShortcutsStore((state) => {
47-
return state.modifierKey;
48-
});
4944
const voiceInput = useShortcutsStore((state) => state.voiceInput);
5045

5146
const { wavesurfer } = useWavesurfer({
@@ -113,10 +108,6 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
113108
}, 1000);
114109
}, [state.isRecording]);
115110

116-
useKeyPress(`${modifierKey}.${voiceInput}`, () => {
117-
startRecording();
118-
});
119-
120111
const getAvailableAudioDevices = async () => {
121112
state.audioDevices = await RecordPlugin.getAvailableAudioDevices();
122113
};
@@ -171,23 +162,9 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
171162
}
172163
)}
173164
>
174-
<Mic
175-
className={clsx("size-4 text-[#999]", {
176-
hidden: modifierKeyPressed,
177-
})}
178-
onClick={startRecording}
179-
/>
180-
181-
<div
182-
className={clsx(
183-
"w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]",
184-
{
185-
hidden: !modifierKeyPressed,
186-
}
187-
)}
188-
>
189-
{voiceInput}
190-
</div>
165+
<VisibleKey shortcut={voiceInput} onKeypress={startRecording}>
166+
<Mic className="size-4 text-[#999]" onClick={startRecording} />
167+
</VisibleKey>
191168
</div>
192169

193170
<div

0 commit comments

Comments
 (0)