mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-13 04:33:42 +08:00
feat: support voice input
This commit is contained in:
@@ -10,6 +10,7 @@ import React, {
|
||||
} from "react";
|
||||
|
||||
import SendWhiteIcon from "../icons/send-white.svg";
|
||||
import VoiceWhiteIcon from "../icons/voice-white.svg";
|
||||
import BrainIcon from "../icons/brain.svg";
|
||||
import RenameIcon from "../icons/rename.svg";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
@@ -73,7 +74,7 @@ import dynamic from "next/dynamic";
|
||||
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import { Prompt, usePromptStore } from "../store/prompt";
|
||||
import Locale from "../locales";
|
||||
import Locale, { getLang, getSTTLang } from "../locales";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
import styles from "./chat.module.scss";
|
||||
@@ -799,6 +800,27 @@ function _Chat() {
|
||||
}
|
||||
};
|
||||
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const [recognition, setRecognition] = useState<any>(null);
|
||||
const startListening = () => {
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
setIsListening(true);
|
||||
}
|
||||
};
|
||||
|
||||
const stopListening = () => {
|
||||
if (recognition) {
|
||||
recognition.stop();
|
||||
setIsListening(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRecognitionEnd = (finalTranscript: string) => {
|
||||
console.log(finalTranscript);
|
||||
if (finalTranscript) setUserInput(finalTranscript);
|
||||
};
|
||||
|
||||
const doSubmit = (userInput: string) => {
|
||||
if (userInput.trim() === "") return;
|
||||
const matchCommand = chatCommands.match(userInput);
|
||||
@@ -869,6 +891,26 @@ function _Chat() {
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
if (typeof window !== "undefined") {
|
||||
const SpeechRecognition =
|
||||
(window as any).SpeechRecognition ||
|
||||
(window as any).webkitSpeechRecognition;
|
||||
const recognitionInstance = new SpeechRecognition();
|
||||
recognitionInstance.continuous = true;
|
||||
recognitionInstance.interimResults = true;
|
||||
let lang = getSTTLang();
|
||||
recognitionInstance.lang = lang;
|
||||
recognitionInstance.onresult = (event: any) => {
|
||||
const result = event.results[event.results.length - 1];
|
||||
if (result.isFinal) {
|
||||
if (!isListening) {
|
||||
onRecognitionEnd(result[0].transcript);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setRecognition(recognitionInstance);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// check if should send message
|
||||
@@ -1649,13 +1691,26 @@ function _Chat() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
|
||||
{config.sttConfig.enable ? (
|
||||
<IconButton
|
||||
icon={<VoiceWhiteIcon />}
|
||||
text={
|
||||
isListening ? Locale.Chat.StopSpeak : Locale.Chat.StartSpeak
|
||||
}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => (isListening ? stopListening() : startListening())}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ import { PluginConfigList } from "./plugin-config";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { ProviderType } from "../utils/cloud";
|
||||
import { TTSConfigList } from "./tts-config";
|
||||
import { STTConfigList } from "./stt-config";
|
||||
|
||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
const promptStore = usePromptStore();
|
||||
@@ -1212,6 +1213,17 @@ export function Settings() {
|
||||
/>
|
||||
</List>
|
||||
|
||||
<List>
|
||||
<STTConfigList
|
||||
sttConfig={config.sttConfig}
|
||||
updateConfig={(updater) => {
|
||||
const sttConfig = { ...config.sttConfig };
|
||||
updater(sttConfig);
|
||||
config.update((config) => (config.sttConfig = sttConfig));
|
||||
}}
|
||||
/>
|
||||
</List>
|
||||
|
||||
<DangerItems />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
28
app/components/stt-config.tsx
Normal file
28
app/components/stt-config.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { STTConfig } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { ListItem } from "./ui-lib";
|
||||
|
||||
export function STTConfigList(props: {
|
||||
sttConfig: STTConfig;
|
||||
updateConfig: (updater: (config: STTConfig) => void) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.STT.Enable.Title}
|
||||
subTitle={Locale.Settings.STT.Enable.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.sttConfig.enable}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.enable = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
119
app/components/stt.module.scss
Normal file
119
app/components/stt.module.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
@import "../styles/animation.scss";
|
||||
.plugin-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.plugin-page-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
.plugin-filter {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
animation: slide-in ease 0.3s;
|
||||
height: 40px;
|
||||
|
||||
display: flex;
|
||||
|
||||
.search-bar {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-bar:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.plugin-filter-lang {
|
||||
height: 100%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.plugin-create {
|
||||
height: 100%;
|
||||
margin-left: 10px;
|
||||
box-sizing: border-box;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border: var(--border-in-light);
|
||||
animation: slide-in ease 0.3s;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.plugin-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.plugin-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.plugin-title {
|
||||
.plugin-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.plugin-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
.plugin-runtime-warning {
|
||||
font-size: 12px;
|
||||
color: #f86c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-actions {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
transition: all ease 0.3s;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: var(--card-shadow);
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: var(--border-in-light);
|
||||
}
|
||||
|
||||
.plugin-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user