mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-17 06:23:44 +08:00
Merge 0ea650d958 into 6ded4e96e7
This commit is contained in:
@@ -75,6 +75,14 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.listening {
|
||||
width: var(--full-width);
|
||||
.text {
|
||||
opacity: 1;
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--delay: 0.5s;
|
||||
width: var(--full-width);
|
||||
|
||||
@@ -10,6 +10,8 @@ import React, {
|
||||
} from "react";
|
||||
|
||||
import SendWhiteIcon from "../icons/send-white.svg";
|
||||
import VoiceOpenIcon from "../icons/vioce-open.svg";
|
||||
import VoiceCloseIcon from "../icons/vioce-close.svg";
|
||||
import BrainIcon from "../icons/brain.svg";
|
||||
import RenameIcon from "../icons/rename.svg";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
@@ -72,6 +74,7 @@ import {
|
||||
isDalle3,
|
||||
showPlugins,
|
||||
safeLocalStorage,
|
||||
isFirefox,
|
||||
} from "../utils";
|
||||
|
||||
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
||||
@@ -98,7 +101,9 @@ import {
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CHAT_PAGE_SIZE,
|
||||
DEFAULT_STT_ENGINE,
|
||||
DEFAULT_TTS_ENGINE,
|
||||
FIREFOX_DEFAULT_STT_ENGINE,
|
||||
ModelProvider,
|
||||
Path,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
@@ -117,6 +122,7 @@ import { MultimodalContent } from "../client/api";
|
||||
|
||||
import { ClientApi } from "../client/api";
|
||||
import { createTTSPlayer } from "../utils/audio";
|
||||
import { OpenAITranscriptionApi, WebTranscriptionApi } from "../utils/speech";
|
||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
||||
|
||||
import { isEmpty } from "lodash-es";
|
||||
@@ -374,6 +380,7 @@ export function ChatAction(props: {
|
||||
text: string;
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
isListening?: boolean;
|
||||
}) {
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
@@ -395,7 +402,9 @@ export function ChatAction(props: {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles["chat-input-action"]} clickable`}
|
||||
className={`${styles["chat-input-action"]} clickable ${
|
||||
props.isListening ? styles["listening"] : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
props.onClick();
|
||||
setTimeout(updateWidth, 1);
|
||||
@@ -553,6 +562,61 @@ export function ChatActions(props: {
|
||||
}
|
||||
}, [chatStore, currentModel, models, session]);
|
||||
|
||||
const [isListening, setIsListening] = useState(false);
|
||||
const [isTranscription, setIsTranscription] = useState(false);
|
||||
const [speechApi, setSpeechApi] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirefox()) config.sttConfig.engine = FIREFOX_DEFAULT_STT_ENGINE;
|
||||
const lang = config.sttConfig.lang;
|
||||
setSpeechApi(
|
||||
config.sttConfig.engine !== DEFAULT_STT_ENGINE
|
||||
? new WebTranscriptionApi(
|
||||
(transcription) => onRecognitionEnd(transcription),
|
||||
lang,
|
||||
)
|
||||
: new OpenAITranscriptionApi((transcription) =>
|
||||
onRecognitionEnd(transcription),
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
function playSound(fileName: string) {
|
||||
const audio = new Audio(fileName);
|
||||
audio.play().catch((error) => {
|
||||
console.error("error:", error);
|
||||
});
|
||||
}
|
||||
|
||||
const startListening = async () => {
|
||||
playSound("/Recordingstart.mp3");
|
||||
showToast(Locale.Chat.StartSpeak);
|
||||
if (speechApi) {
|
||||
await speechApi.start();
|
||||
setIsListening(true);
|
||||
document.getElementById("chat-input")?.focus();
|
||||
}
|
||||
};
|
||||
const stopListening = async () => {
|
||||
showToast(Locale.Chat.CloseSpeak);
|
||||
if (speechApi) {
|
||||
if (config.sttConfig.engine !== DEFAULT_STT_ENGINE)
|
||||
setIsTranscription(true);
|
||||
await speechApi.stop();
|
||||
setIsListening(false);
|
||||
}
|
||||
playSound("/Recordingdone.mp3");
|
||||
document.getElementById("chat-input")?.focus();
|
||||
};
|
||||
const onRecognitionEnd = (finalTranscript: string) => {
|
||||
console.log(finalTranscript);
|
||||
if (finalTranscript) {
|
||||
props.setUserInput((prevInput) => prevInput + finalTranscript);
|
||||
}
|
||||
if (config.sttConfig.engine !== DEFAULT_STT_ENGINE)
|
||||
setIsTranscription(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["chat-input-actions"]}>
|
||||
{couldStop && (
|
||||
@@ -787,6 +851,17 @@ export function ChatActions(props: {
|
||||
icon={<ShortcutkeyIcon />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{config.sttConfig.enable && (
|
||||
<ChatAction
|
||||
onClick={async () =>
|
||||
isListening ? await stopListening() : await startListening()
|
||||
}
|
||||
text={isListening ? Locale.Chat.StopSpeak : Locale.Chat.StartSpeak}
|
||||
icon={isListening ? <VoiceOpenIcon /> : <VoiceCloseIcon />}
|
||||
isListening={isListening}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1516,7 +1591,7 @@ function _Chat() {
|
||||
setAttachImages(images);
|
||||
}
|
||||
|
||||
// 快捷键 shortcut keys
|
||||
// 快捷键
|
||||
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -193,7 +193,9 @@ function CustomCode(props: { children: any; className?: string }) {
|
||||
const renderShowMoreButton = () => {
|
||||
if (showToggle && enableCodeFold && collapsed) {
|
||||
return (
|
||||
<div className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}>
|
||||
<div
|
||||
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
|
||||
>
|
||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -85,6 +85,7 @@ import { nanoid } from "nanoid";
|
||||
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();
|
||||
@@ -1811,6 +1812,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>
|
||||
|
||||
75
app/components/stt-config.tsx
Normal file
75
app/components/stt-config.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { STTConfig, STTConfigValidator } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { ListItem, Select } from "./ui-lib";
|
||||
import { DEFAULT_STT_ENGINES, DEFAULT_STT_LANGUAGES } from "../constant";
|
||||
import { isFirefox } from "../utils";
|
||||
|
||||
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>
|
||||
<ListItem title={Locale.Settings.STT.Engine.Title}>
|
||||
<Select
|
||||
value={props.sttConfig.engine}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.engine = STTConfigValidator.engine(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{isFirefox()
|
||||
? DEFAULT_STT_ENGINES.filter((v) => v !== "Web Speech API").map(
|
||||
(v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
),
|
||||
)
|
||||
: DEFAULT_STT_ENGINES.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
{props.sttConfig.engine === "Web Speech API" && !isFirefox() && (
|
||||
<ListItem title="语言选择">
|
||||
<Select
|
||||
value={props.sttConfig.lang}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) => (config.lang = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{DEFAULT_STT_LANGUAGES.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</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