Skip to content

Commit 89a763d

Browse files
authored
feat: supports keyboard shortcuts with immediate effect (#316)
* feat: supports keyboard shortcuts with immediate effect * feat: customize mode switching shortcuts * refactor: remove the shift * fix: voice input audio input device number anomaly issue * feat: support for changing the focus state of the input box * refactor: shortcuts for handling input box focus separately * feat: upload file support shortcuts * refactor: the connection timeout is specified with the variable * refactor: shortcut keys to modify the input box before displaying modifier keys * docs: update changelog * style: remove useless import * refactor: window focus changes modifier key press status to false * refactor: correcting errors of judgment * docs: update changelog
1 parent 0c42a51 commit 89a763d

13 files changed

Lines changed: 309 additions & 139 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ Information about release notes of Coco Server is provided here.
1212
### Breaking changes
1313

1414
- feat: add web pages components #277
15+
- feat: support for customizing some of the preset shortcuts #316
1516

1617
### Features
1718

1819
- feat: chat mode support for uploading files #310
1920
- feat: support multi websocket connections #314
2021
- feat: add support for embeddable web widget #277
2122

22-
2323
### Bug fix
2424

2525
### Improvements

src/components/AudioRecording/index.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useAppStore } from "@/stores/appStore";
2-
import { useReactive } from "ahooks";
2+
import { useKeyPress, useReactive } from "ahooks";
33
import clsx from "clsx";
44
import { Check, Loader, Mic, X } from "lucide-react";
55
import { FC, useEffect, useRef } from "react";
@@ -11,6 +11,7 @@ import { useWavesurfer } from "@wavesurfer/react";
1111
import RecordPlugin from "wavesurfer.js/dist/plugins/record.esm.js";
1212
import { transcription } from "@/api/transcription";
1313
import { useConnectStore } from "@/stores/connectStore";
14+
import { useShortcutsStore } from "@/stores/shortcutsStore";
1415

1516
interface AudioRecordingProps {
1617
onChange?: (text: string) => void;
@@ -39,6 +40,13 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
3940
const recordRef = useRef<RecordPlugin>();
4041
const withVisibility = useAppStore((state) => state.withVisibility);
4142
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+
});
49+
const voiceInput = useShortcutsStore((state) => state.voiceInput);
4250

4351
const { wavesurfer } = useWavesurfer({
4452
container: containerRef,
@@ -67,6 +75,8 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
6775
);
6876

6977
record.on("record-end", (blob) => {
78+
if (!state.converting) return;
79+
7080
const reader = new FileReader();
7181

7282
reader.onloadend = async () => {
@@ -103,14 +113,22 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
103113
}, 1000);
104114
}, [state.isRecording]);
105115

116+
useKeyPress(`${modifierKey}.${voiceInput}`, () => {
117+
startRecording();
118+
});
119+
106120
const getAvailableAudioDevices = async () => {
107121
state.audioDevices = await RecordPlugin.getAvailableAudioDevices();
108122
};
109123

110124
const resetState = (otherState: Partial<State> = {}) => {
111125
clearInterval(interval);
112126
recordRef.current?.stopRecording();
113-
Object.assign(state, { ...INITIAL_STATE, ...otherState });
127+
Object.assign(state, {
128+
...INITIAL_STATE,
129+
...otherState,
130+
audioDevices: state.audioDevices,
131+
});
114132
};
115133

116134
const checkPermission = async () => {
@@ -153,7 +171,23 @@ const AudioRecording: FC<AudioRecordingProps> = (props) => {
153171
}
154172
)}
155173
>
156-
<Mic className="size-4 text-[#999]" onClick={startRecording} />
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>
157191
</div>
158192

159193
<div

src/components/Common/ChatSwitch.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
11
import React, { useEffect, useCallback } from "react";
22
import { Bot, Search } from "lucide-react";
33

4-
import { isMetaOrCtrlKey } from "@/utils/keyboardUtils";
4+
import { useShortcutsStore } from "@/stores/shortcutsStore";
55

66
interface ChatSwitchProps {
77
isChatMode: boolean;
88
onChange: (isChatMode: boolean) => void;
99
}
1010

1111
const ChatSwitch: React.FC<ChatSwitchProps> = ({ isChatMode, onChange }) => {
12+
const modifierKeyPressed = useShortcutsStore((state) => {
13+
return state.modifierKeyPressed;
14+
});
15+
const modeSwitch = useShortcutsStore((state) => {
16+
return state.modeSwitch;
17+
});
18+
1219
const handleToggle = useCallback(() => {
1320
onChange?.(!isChatMode);
1421
}, [onChange, isChatMode]);
1522

1623
const handleKeydown = useCallback(
1724
(event: KeyboardEvent) => {
18-
if (isMetaOrCtrlKey(event) && event.key === "t") {
25+
if (modifierKeyPressed && event.key === modeSwitch.toLowerCase()) {
1926
event.preventDefault();
2027
// console.log("Switch mode triggered");
2128
handleToggle();
2229
}
2330
},
24-
[handleToggle]
31+
[handleToggle, modifierKeyPressed, modeSwitch]
2532
);
2633

2734
useEffect(() => {

src/components/Search/InputBox.tsx

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { hide_coco } from "@/commands";
1616
import { DataSource } from "@/types/commands";
1717
import InputExtra from "./InputExtra";
1818
import { useConnectStore } from "@/stores/connectStore";
19+
import { useShortcutsStore } from "@/stores/shortcutsStore";
20+
import { useKeyPress } from "ahooks";
1921

2022
interface ChatInputProps {
2123
onSend: (message: string) => void;
@@ -73,7 +75,7 @@ export default function ChatInput({
7375
getFileIcon,
7476
}: ChatInputProps) {
7577
const { t } = useTranslation();
76-
78+
7779
const showTooltip = useAppStore(
7880
(state: { showTooltip: boolean }) => state.showTooltip
7981
);
@@ -88,6 +90,14 @@ export default function ChatInput({
8890
);
8991

9092
const sessionId = useConnectStore((state) => state.currentSessionId);
93+
const modifierKey = useShortcutsStore((state) => {
94+
return state.modifierKey;
95+
});
96+
const modifierKeyPressed = useShortcutsStore((state) => {
97+
return state.modifierKeyPressed;
98+
});
99+
const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
100+
const returnToInput = useShortcutsStore((state) => state.returnToInput);
91101

92102
useEffect(() => {
93103
return () => {
@@ -109,7 +119,7 @@ export default function ChatInput({
109119
setReconnectCountdown(0);
110120
return;
111121
}
112-
122+
113123
if (reconnectCountdown > 0) {
114124
const timer = setTimeout(() => {
115125
setReconnectCountdown(reconnectCountdown - 1);
@@ -119,6 +129,22 @@ export default function ChatInput({
119129
}, [reconnectCountdown, connected]);
120130

121131
const [isCommandPressed, setIsCommandPressed] = useState(false);
132+
const setModifierKeyPressed = useShortcutsStore((state) => {
133+
return state.setModifierKeyPressed;
134+
});
135+
136+
useEffect(() => {
137+
const handleFocus = () => {
138+
setIsCommandPressed(false);
139+
setModifierKeyPressed(false);
140+
};
141+
142+
window.addEventListener("focus", handleFocus);
143+
144+
return () => {
145+
window.removeEventListener("focus", handleFocus);
146+
};
147+
}, []);
122148

123149
const handleToggleFocus = useCallback(() => {
124150
if (isChatMode) {
@@ -146,6 +172,8 @@ export default function ChatInput({
146172
}
147173
}, [inputValue, isPinned]);
148174

175+
useKeyPress(`${modifierKey}.${returnToInput}`, handleToggleFocus);
176+
149177
const handleKeyDown = useCallback(
150178
(e: KeyboardEvent) => {
151179
// console.log("handleKeyDown", e.code, e.key);
@@ -167,8 +195,6 @@ export default function ChatInput({
167195
case "Comma":
168196
setIsCommandPressed(false);
169197
break;
170-
case "KeyI":
171-
handleToggleFocus();
172198
break;
173199
case "ArrowLeft":
174200
setSourceData(undefined);
@@ -299,13 +325,13 @@ export default function ChatInput({
299325
300326
</div>
301327
) : null}
302-
{showTooltip && isCommandPressed ? (
328+
{showTooltip && modifierKeyPressed ? (
303329
<div
304330
className={`absolute ${
305331
!isChatMode && sourceData ? "left-7" : ""
306332
} 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_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
307333
>
308-
I
334+
{returnToInput}
309335
</div>
310336
) : null}
311337
</div>
@@ -344,13 +370,13 @@ export default function ChatInput({
344370
</button>
345371
) : null}
346372

347-
{showTooltip && isChatMode && isCommandPressed ? (
373+
{/* {showTooltip && isChatMode && isCommandPressed ? (
348374
<div
349375
className={`absolute right-10 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]`}
350376
>
351377
M
352378
</div>
353-
) : null}
379+
) : null} */}
354380

355381
{showTooltip && isChatMode && isCommandPressed ? (
356382
<div
@@ -371,7 +397,7 @@ export default function ChatInput({
371397
}}
372398
>
373399
{reconnectCountdown > 0
374-
? `${t("search.input.connecting")}(${reconnectCountdown}s)`
400+
? `${t("search.input.connecting")}(${reconnectCountdown}s)`
375401
: t("search.input.reconnect")}
376402
</div>
377403
</div>
@@ -440,11 +466,11 @@ export default function ChatInput({
440466

441467
{isChatPage ? null : (
442468
<div className="relative w-16 flex justify-end items-center">
443-
{showTooltip && isCommandPressed ? (
469+
{showTooltip && modifierKeyPressed ? (
444470
<div
445471
className={`absolute left-1 z-10 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]`}
446472
>
447-
T
473+
{modeSwitch}
448474
</div>
449475
) : null}
450476
<ChatSwitch

src/components/Search/InputExtra.tsx

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import {
1212
} from "@headlessui/react";
1313
import { castArray, find, isNil } from "lodash-es";
1414
import { nanoid } from "nanoid";
15-
import { useCreation, useMount, useReactive } from "ahooks";
15+
import { useCreation, useKeyPress, useMount, useReactive } from "ahooks";
1616

1717
import { useChatStore } from "@/stores/chatStore";
1818
import { useAppStore } from "@/stores/appStore";
1919
import Tooltip from "@/components/Common/Tooltip";
20+
import { useShortcutsStore } from "@/stores/shortcutsStore";
21+
import clsx from "clsx";
2022

2123
interface State {
2224
screenRecordingPermission?: boolean;
@@ -62,6 +64,15 @@ const InputExtra = ({
6264
const uploadFiles = useChatStore((state) => state.uploadFiles);
6365
const setUploadFiles = useChatStore((state) => state.setUploadFiles);
6466
const withVisibility = useAppStore((state) => state.withVisibility);
67+
const modifierKey = useShortcutsStore((state) => {
68+
return state.modifierKey;
69+
});
70+
const addFile = useShortcutsStore((state) => {
71+
return state.addFile;
72+
});
73+
const modifierKeyPressed = useShortcutsStore((state) => {
74+
return state.modifierKeyPressed;
75+
});
6576

6677
const state = useReactive<State>({
6778
screenshotableMonitors: [],
@@ -72,6 +83,18 @@ const InputExtra = ({
7283
state.screenRecordingPermission = await checkScreenPermission();
7384
});
7485

86+
const handleSelectFile = async () => {
87+
const selectedFiles = await withVisibility(() => {
88+
return openFileDialog({
89+
multiple: true,
90+
});
91+
});
92+
93+
if (isNil(selectedFiles)) return;
94+
95+
handleUploadFiles(selectedFiles);
96+
};
97+
7598
const handleUploadFiles = async (paths: string | string[]) => {
7699
const files: typeof uploadFiles = [];
77100

@@ -99,17 +122,7 @@ const InputExtra = ({
99122
const menuItems: MenuItem[] = [
100123
{
101124
label: t("search.input.uploadFile"),
102-
clickEvent: async () => {
103-
const selectedFiles = await withVisibility(() => {
104-
return openFileDialog({
105-
multiple: true,
106-
});
107-
});
108-
109-
if (isNil(selectedFiles)) return;
110-
111-
handleUploadFiles(selectedFiles);
112-
},
125+
clickEvent: handleSelectFile,
113126
},
114127
{
115128
label: t("search.input.screenshot"),
@@ -167,12 +180,29 @@ const InputExtra = ({
167180
i18n.language,
168181
]);
169182

183+
useKeyPress(`${modifierKey}.${addFile}`, handleSelectFile);
184+
170185
return (
171186
<Menu>
172-
<MenuButton>
187+
<MenuButton className="size-6">
173188
<Tooltip content="支持截图、上传文件,最多 50个,单个文件最大 100 MB。">
174-
<div className="size-6 flex justify-center items-center rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
175-
<Plus className="size-5" />
189+
<div className="size-full flex justify-center items-center rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
190+
<Plus
191+
className={clsx("size-5", {
192+
hidden: modifierKeyPressed,
193+
})}
194+
/>
195+
196+
<div
197+
className={clsx(
198+
"size-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]",
199+
{
200+
hidden: !modifierKeyPressed,
201+
}
202+
)}
203+
>
204+
{addFile}
205+
</div>
176206
</div>
177207
</Tooltip>
178208
</MenuButton>

src/components/Search/NoResults.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ export const NoResults = () => {
3535
</div>
3636
</div>
3737
);
38-
};
38+
};

0 commit comments

Comments
 (0)