mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-17 22:43:42 +08:00
Merge branch 'ChatGPTNextWeb:main' into main
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useWindowSize } from "@/app/utils";
|
||||
import { IconButton } from "./button";
|
||||
import { nanoid } from "nanoid";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
import {
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
import { useChatStore } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
import { MaskAvatar } from "./mask";
|
||||
import { Mask } from "../store/mask";
|
||||
|
||||
@@ -15,6 +15,8 @@ import RenameIcon from "../icons/rename.svg";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
import ReturnIcon from "../icons/return.svg";
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
import SpeakIcon from "../icons/speak.svg";
|
||||
import SpeakStopIcon from "../icons/speak-stop.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import LoadingButtonIcon from "../icons/loading.svg";
|
||||
import PromptIcon from "../icons/prompt.svg";
|
||||
@@ -96,7 +98,8 @@ import {
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CHAT_PAGE_SIZE,
|
||||
LAST_INPUT_KEY,
|
||||
DEFAULT_TTS_ENGINE,
|
||||
ModelProvider,
|
||||
Path,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
UNFINISHED_INPUT,
|
||||
@@ -113,6 +116,11 @@ import { useAllModels } from "../utils/hooks";
|
||||
import { MultimodalContent } from "../client/api";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
import { ClientApi } from "../client/api";
|
||||
import { createTTSPlayer } from "../utils/audio";
|
||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
||||
|
||||
const ttsPlayer = createTTSPlayer();
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
@@ -443,6 +451,7 @@ export function ChatActions(props: {
|
||||
hitBottom: boolean;
|
||||
uploading: boolean;
|
||||
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setUserInput: (input: string) => void;
|
||||
}) {
|
||||
const config = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
@@ -981,6 +990,7 @@ function _Chat() {
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.clearContextIndex = session.messages.length),
|
||||
),
|
||||
fork: () => chatStore.forkSession(),
|
||||
del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
|
||||
});
|
||||
|
||||
@@ -1184,10 +1194,55 @@ function _Chat() {
|
||||
});
|
||||
};
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const [speechStatus, setSpeechStatus] = useState(false);
|
||||
const [speechLoading, setSpeechLoading] = useState(false);
|
||||
async function openaiSpeech(text: string) {
|
||||
if (speechStatus) {
|
||||
ttsPlayer.stop();
|
||||
setSpeechStatus(false);
|
||||
} else {
|
||||
var api: ClientApi;
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
const config = useAppConfig.getState();
|
||||
setSpeechLoading(true);
|
||||
ttsPlayer.init();
|
||||
let audioBuffer: ArrayBuffer;
|
||||
const { markdownToTxt } = require("markdown-to-txt");
|
||||
const textContent = markdownToTxt(text);
|
||||
if (config.ttsConfig.engine !== DEFAULT_TTS_ENGINE) {
|
||||
const edgeVoiceName = accessStore.edgeVoiceName();
|
||||
const tts = new MsEdgeTTS();
|
||||
await tts.setMetadata(
|
||||
edgeVoiceName,
|
||||
OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3,
|
||||
);
|
||||
audioBuffer = await tts.toArrayBuffer(textContent);
|
||||
} else {
|
||||
audioBuffer = await api.llm.speech({
|
||||
model: config.ttsConfig.model,
|
||||
input: textContent,
|
||||
voice: config.ttsConfig.voice,
|
||||
speed: config.ttsConfig.speed,
|
||||
});
|
||||
}
|
||||
setSpeechStatus(true);
|
||||
ttsPlayer
|
||||
.play(audioBuffer, () => {
|
||||
setSpeechStatus(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("[OpenAI Speech]", e);
|
||||
showToast(prettyObject(e));
|
||||
setSpeechStatus(false);
|
||||
})
|
||||
.finally(() => setSpeechLoading(false));
|
||||
}
|
||||
}
|
||||
|
||||
const context: RenderMessage[] = useMemo(() => {
|
||||
return session.mask.hideContext ? [] : session.mask.context.slice();
|
||||
}, [session.mask.context, session.mask.hideContext]);
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
if (
|
||||
context.length === 0 &&
|
||||
@@ -1724,6 +1779,25 @@ function _Chat() {
|
||||
)
|
||||
}
|
||||
/>
|
||||
{config.ttsConfig.enable && (
|
||||
<ChatAction
|
||||
text={
|
||||
speechStatus
|
||||
? Locale.Chat.Actions.StopSpeech
|
||||
: Locale.Chat.Actions.Speech
|
||||
}
|
||||
icon={
|
||||
speechStatus ? (
|
||||
<SpeakStopIcon />
|
||||
) : (
|
||||
<SpeakIcon />
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
openaiSpeech(getMessageTextContent(message))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1842,6 +1916,7 @@ function _Chat() {
|
||||
onSearch("");
|
||||
}}
|
||||
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
||||
setUserInput={setUserInput}
|
||||
/>
|
||||
<label
|
||||
className={`${styles["chat-input-panel-inner"]} ${
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
import Locale from "../locales";
|
||||
import styles from "./exporter.module.scss";
|
||||
import {
|
||||
|
||||
@@ -37,7 +37,7 @@ import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import chatStyle from "./chat.module.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
@@ -48,7 +48,6 @@ import { Updater } from "../typing";
|
||||
import { ModelConfigList } from "./model-config";
|
||||
import { FileName, Path } from "../constant";
|
||||
import { BUILTIN_MASK_STORE } from "../masks";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "./ui-lib";
|
||||
import Locale from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export function PluginPage() {
|
||||
|
||||
@@ -80,6 +80,7 @@ import { useSyncStore } from "../store/sync";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { ProviderType } from "../utils/cloud";
|
||||
import { TTSConfigList } from "./tts-config";
|
||||
|
||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
const promptStore = usePromptStore();
|
||||
@@ -1646,6 +1647,17 @@ export function Settings() {
|
||||
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
||||
)}
|
||||
|
||||
<List>
|
||||
<TTSConfigList
|
||||
ttsConfig={config.ttsConfig}
|
||||
updateConfig={(updater) => {
|
||||
const ttsConfig = { ...config.ttsConfig };
|
||||
updater(ttsConfig);
|
||||
config.update((config) => (config.ttsConfig = ttsConfig));
|
||||
}}
|
||||
/>
|
||||
</List>
|
||||
|
||||
<DangerItems />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -7,7 +7,6 @@ import SettingsIcon from "../icons/settings.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import MaskIcon from "../icons/mask.svg";
|
||||
import DragIcon from "../icons/drag.svg";
|
||||
@@ -257,11 +256,6 @@ export function SideBar(props: { className?: string }) {
|
||||
{showPluginSelector && (
|
||||
<Selector
|
||||
items={[
|
||||
{
|
||||
title: "👇 Please select the plugin you need to use",
|
||||
value: "-",
|
||||
disable: true,
|
||||
},
|
||||
...PLUGINS.map((item) => {
|
||||
return {
|
||||
title: item.name,
|
||||
|
||||
133
app/components/tts-config.tsx
Normal file
133
app/components/tts-config.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { TTSConfig, TTSConfigValidator } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { ListItem, Select } from "./ui-lib";
|
||||
import {
|
||||
DEFAULT_TTS_ENGINE,
|
||||
DEFAULT_TTS_ENGINES,
|
||||
DEFAULT_TTS_MODELS,
|
||||
DEFAULT_TTS_VOICES,
|
||||
} from "../constant";
|
||||
import { InputRange } from "./input-range";
|
||||
|
||||
export function TTSConfigList(props: {
|
||||
ttsConfig: TTSConfig;
|
||||
updateConfig: (updater: (config: TTSConfig) => void) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.TTS.Enable.Title}
|
||||
subTitle={Locale.Settings.TTS.Enable.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.ttsConfig.enable}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.enable = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
{/* <ListItem
|
||||
title={Locale.Settings.TTS.Autoplay.Title}
|
||||
subTitle={Locale.Settings.TTS.Autoplay.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.ttsConfig.autoplay}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.autoplay = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem> */}
|
||||
<ListItem title={Locale.Settings.TTS.Engine}>
|
||||
<Select
|
||||
value={props.ttsConfig.engine}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.engine = TTSConfigValidator.engine(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{DEFAULT_TTS_ENGINES.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
{props.ttsConfig.engine === DEFAULT_TTS_ENGINE && (
|
||||
<>
|
||||
<ListItem title={Locale.Settings.TTS.Model}>
|
||||
<Select
|
||||
value={props.ttsConfig.model}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.model = TTSConfigValidator.model(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{DEFAULT_TTS_MODELS.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.TTS.Voice.Title}
|
||||
subTitle={Locale.Settings.TTS.Voice.SubTitle}
|
||||
>
|
||||
<Select
|
||||
value={props.ttsConfig.voice}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.voice = TTSConfigValidator.voice(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{DEFAULT_TTS_VOICES.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.TTS.Speed.Title}
|
||||
subTitle={Locale.Settings.TTS.Speed.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
aria={Locale.Settings.TTS.Speed.Title}
|
||||
value={props.ttsConfig.speed?.toFixed(1)}
|
||||
min="0.3"
|
||||
max="4.0"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.speed = TTSConfigValidator.speed(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
119
app/components/tts.module.scss
Normal file
119
app/components/tts.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