From e44ebe3f0eda9ab6f08dc6a58601e333dd46101b Mon Sep 17 00:00:00 2001 From: Dogtiti <499960698@qq.com> Date: Thu, 7 Nov 2024 21:28:23 +0800 Subject: [PATCH] feat: realtime config --- app/components/chat.tsx | 12 +- .../realtime-chat/realtime-chat.tsx | 98 +++++----- .../realtime-chat/realtime-config.tsx | 173 ++++++++++++++++++ app/components/settings.tsx | 14 +- app/locales/cn.ts | 33 ++++ app/locales/en.ts | 33 ++++ app/store/config.ts | 15 ++ 7 files changed, 320 insertions(+), 58 deletions(-) create mode 100644 app/components/realtime-chat/realtime-config.tsx diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 275edaddcca..ed51d926f4e 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -793,11 +793,13 @@ export function ChatActions(props: { )}
- props.setShowChatSidePanel(true)} - text={"Realtime Chat"} - icon={} - /> + {config.realtimeConfig.enable && ( + props.setShowChatSidePanel(true)} + text={"Realtime Chat"} + icon={} + /> + )}
); diff --git a/app/components/realtime-chat/realtime-chat.tsx b/app/components/realtime-chat/realtime-chat.tsx index e9620cb39b9..2eb8d3e3740 100644 --- a/app/components/realtime-chat/realtime-chat.tsx +++ b/app/components/realtime-chat/realtime-chat.tsx @@ -1,4 +1,3 @@ -import { useDebouncedCallback } from "use-debounce"; import VoiceIcon from "@/app/icons/voice.svg"; import VoiceOffIcon from "@/app/icons/voice-off.svg"; import PowerIcon from "@/app/icons/power.svg"; @@ -8,12 +7,7 @@ import clsx from "clsx"; import { useState, useRef, useEffect } from "react"; -import { - useAccessStore, - useChatStore, - ChatMessage, - createMessage, -} from "@/app/store"; +import { useChatStore, createMessage, useAppConfig } from "@/app/store"; import { IconButton } from "@/app/components/button"; @@ -23,7 +17,6 @@ import { RTInputAudioItem, RTResponse, TurnDetection, - Voice, } from "rt-client"; import { AudioHandler } from "@/app/lib/audio"; import { uploadImage } from "@/app/utils/chat"; @@ -39,41 +32,40 @@ export function RealtimeChat({ onStartVoice, onPausedVoice, }: RealtimeChatProps) { - const currentItemId = useRef(""); - const currentBotMessage = useRef(); - const currentUserMessage = useRef(); - const accessStore = useAccessStore.getState(); const chatStore = useChatStore(); const session = chatStore.currentSession(); - + const config = useAppConfig(); const [status, setStatus] = useState(""); const [isRecording, setIsRecording] = useState(false); const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [modality, setModality] = useState("audio"); - const [isAzure, setIsAzure] = useState(false); - const [endpoint, setEndpoint] = useState(""); - const [deployment, setDeployment] = useState(""); const [useVAD, setUseVAD] = useState(true); - const [voice, setVoice] = useState("alloy"); - const [temperature, setTemperature] = useState(0.9); const clientRef = useRef(null); const audioHandlerRef = useRef(null); + const initRef = useRef(false); - const apiKey = accessStore.openaiApiKey; + const temperature = config.realtimeConfig.temperature; + const apiKey = config.realtimeConfig.apiKey; + const model = config.realtimeConfig.model; + const azure = config.realtimeConfig.provider === "Azure"; + const azureEndpoint = config.realtimeConfig.azure.endpoint; + const azureDeployment = config.realtimeConfig.azure.deployment; + const voice = config.realtimeConfig.voice; const handleConnect = async () => { if (isConnecting) return; if (!isConnected) { try { setIsConnecting(true); - clientRef.current = isAzure - ? new RTClient(new URL(endpoint), { key: apiKey }, { deployment }) - : new RTClient( + clientRef.current = azure + ? new RTClient( + new URL(azureEndpoint), { key: apiKey }, - { model: "gpt-4o-realtime-preview-2024-10-01" }, - ); + { deployment: azureDeployment }, + ) + : new RTClient({ key: apiKey }, { model }); const modalities: Modality[] = modality === "audio" ? ["text", "audio"] : ["text"]; const turnDetection: TurnDetection = useVAD @@ -191,7 +183,6 @@ export function RealtimeChat({ const blob = audioHandlerRef.current?.savePlayFile(); uploadImage(blob!).then((audio_url) => { botMessage.audio_url = audio_url; - // botMessage.date = new Date().toLocaleString(); // update text and audio_url chatStore.updateTargetSession(session, (session) => { session.messages = session.messages.concat(); @@ -258,31 +249,32 @@ export function RealtimeChat({ } }; - useEffect( - useDebouncedCallback(() => { - const initAudioHandler = async () => { - const handler = new AudioHandler(); - await handler.initialize(); - audioHandlerRef.current = handler; - await handleConnect(); - await toggleRecording(); - }; + useEffect(() => { + // 防止重复初始化 + if (initRef.current) return; + initRef.current = true; - initAudioHandler().catch((error) => { - setStatus(error); - console.error(error); - }); + const initAudioHandler = async () => { + const handler = new AudioHandler(); + await handler.initialize(); + audioHandlerRef.current = handler; + await handleConnect(); + await toggleRecording(); + }; - return () => { - if (isRecording) { - toggleRecording(); - } - audioHandlerRef.current?.close().catch(console.error); - disconnect(); - }; - }), - [], - ); + initAudioHandler().catch((error) => { + setStatus(error); + console.error(error); + }); + + return () => { + if (isRecording) { + toggleRecording(); + } + audioHandlerRef.current?.close().catch(console.error); + disconnect(); + }; + }, []); // update session params useEffect(() => { @@ -304,7 +296,7 @@ export function RealtimeChat({
@@ -312,10 +304,11 @@ export function RealtimeChat({
: } + icon={isRecording ? : } onClick={toggleRecording} disabled={!isConnected} - type={isRecording ? "danger" : isConnected ? "primary" : null} + shadow + bordered />
{status}
@@ -323,7 +316,8 @@ export function RealtimeChat({ } onClick={handleClose} - type={isConnecting || isConnected ? "danger" : "primary"} + shadow + bordered />
diff --git a/app/components/realtime-chat/realtime-config.tsx b/app/components/realtime-chat/realtime-config.tsx new file mode 100644 index 00000000000..08809afda2f --- /dev/null +++ b/app/components/realtime-chat/realtime-config.tsx @@ -0,0 +1,173 @@ +import { RealtimeConfig } from "@/app/store"; + +import Locale from "@/app/locales"; +import { ListItem, Select, PasswordInput } from "@/app/components/ui-lib"; + +import { InputRange } from "@/app/components/input-range"; +import { Voice } from "rt-client"; +import { ServiceProvider } from "@/app/constant"; + +const providers = [ServiceProvider.OpenAI, ServiceProvider.Azure]; + +const models = ["gpt-4o-realtime-preview-2024-10-01"]; + +const voice = ["alloy", "shimmer", "echo"]; + +export function RealtimeConfigList(props: { + realtimeConfig: RealtimeConfig; + updateConfig: (updater: (config: RealtimeConfig) => void) => void; +}) { + const azureConfigComponent = props.realtimeConfig.provider === + ServiceProvider.Azure && ( + <> + + { + props.updateConfig( + (config) => (config.azure.endpoint = e.currentTarget.value), + ); + }} + /> + + + { + props.updateConfig( + (config) => (config.azure.deployment = e.currentTarget.value), + ); + }} + /> + + + ); + + return ( + <> + + + props.updateConfig( + (config) => (config.enable = e.currentTarget.checked), + ) + } + > + + + {props.realtimeConfig.enable && ( + <> + + + + + + + + { + props.updateConfig( + (config) => (config.apiKey = e.currentTarget.value), + ); + }} + /> + + {azureConfigComponent} + + + + + { + props.updateConfig( + (config) => + (config.temperature = e.currentTarget.valueAsNumber), + ); + }} + > + + + )} + + ); +} diff --git a/app/components/settings.tsx b/app/components/settings.tsx index e2666b5512c..ddbda1b730a 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -85,6 +85,7 @@ import { nanoid } from "nanoid"; import { useMaskStore } from "../store/mask"; import { ProviderType } from "../utils/cloud"; import { TTSConfigList } from "./tts-config"; +import { RealtimeConfigList } from "./realtime-chat/realtime-config"; function EditPromptModal(props: { id: string; onClose: () => void }) { const promptStore = usePromptStore(); @@ -1799,7 +1800,18 @@ export function Settings() { {shouldShowPromptModal && ( setShowPromptModal(false)} /> )} - + + { + const realtimeConfig = { ...config.realtimeConfig }; + updater(realtimeConfig); + config.update( + (config) => (config.realtimeConfig = realtimeConfig), + ); + }} + /> +