mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-14 21:13:47 +08:00
part function about interview
This commit is contained in:
@@ -98,6 +98,65 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-action-voice {
|
||||
// display: inline-flex;
|
||||
// border-radius: 20px;
|
||||
// font-size: 12px;
|
||||
// background-color: var(--white);
|
||||
// color: var(--black);
|
||||
// border: var(--border-in-light);
|
||||
// padding: 4px 10px;
|
||||
// animation: slide-in ease 0.3s;
|
||||
// box-shadow: var(--card-shadow);
|
||||
// transition: width ease 0.3s;
|
||||
// align-items: center;
|
||||
// height: 16px;
|
||||
// width: var(--icon-width);
|
||||
// overflow: hidden;
|
||||
|
||||
display: inline-flex;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
border: var(--border-in-light);
|
||||
padding: 4px 10px;
|
||||
box-shadow: var(--card-shadow);
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
width: var(--full-width); /* 使用全宽度 */
|
||||
// .text {
|
||||
// white-space: nowrap;
|
||||
// padding-left: 5px;
|
||||
// opacity: 0;
|
||||
// transform: translateX(-5px);
|
||||
// transition: all ease 0.3s;
|
||||
// pointer-events: none;
|
||||
// }
|
||||
|
||||
// &:hover {
|
||||
// --delay: 0.5s;
|
||||
// width: var(--full-width);
|
||||
// transition-delay: var(--delay);
|
||||
|
||||
|
||||
// }
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
padding-left: 5px;
|
||||
opacity: 1; /* 确保文本始终可见 */
|
||||
transform: translateX(0); /* 移除初始偏移 */
|
||||
transition: none; /* 移除过渡效果 */
|
||||
}
|
||||
|
||||
.text,
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-toast {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
//#region ignore head
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import React, {
|
||||
Fragment,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { toast, Toaster } from "react-hot-toast";
|
||||
import { debounce } from "lodash";
|
||||
// import { startVoiceDetection } from "../utils/voice-start";
|
||||
import SendWhiteIcon from "../icons/send-white.svg";
|
||||
import BrainIcon from "../icons/brain.svg";
|
||||
import RenameIcon from "../icons/rename.svg";
|
||||
@@ -48,6 +53,7 @@ import PluginIcon from "../icons/plugin.svg";
|
||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||
import McpToolIcon from "../icons/tool.svg";
|
||||
import HeadphoneIcon from "../icons/headphone.svg";
|
||||
import MenuIcon from "../icons/menu.svg";
|
||||
import {
|
||||
BOT_HELLO,
|
||||
ChatMessage,
|
||||
@@ -125,6 +131,7 @@ import { getModelProvider } from "../utils/model";
|
||||
import { RealtimeChat } from "@/app/components/realtime-chat";
|
||||
import clsx from "clsx";
|
||||
import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions";
|
||||
import { InterviewOverlay } from "./interview-overlay";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
@@ -449,47 +456,58 @@ export function ChatAction(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useScrollToBottom(
|
||||
scrollRef: RefObject<HTMLDivElement>,
|
||||
detach: boolean = false,
|
||||
messages: ChatMessage[],
|
||||
) {
|
||||
// for auto-scroll
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const scrollDomToBottom = useCallback(() => {
|
||||
const dom = scrollRef.current;
|
||||
if (dom) {
|
||||
requestAnimationFrame(() => {
|
||||
setAutoScroll(true);
|
||||
dom.scrollTo(0, dom.scrollHeight);
|
||||
});
|
||||
}
|
||||
}, [scrollRef]);
|
||||
|
||||
// auto scroll
|
||||
useEffect(() => {
|
||||
if (autoScroll && !detach) {
|
||||
scrollDomToBottom();
|
||||
}
|
||||
export function ChatActionVoice(props: {
|
||||
text: string;
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState({
|
||||
full: 32,
|
||||
icon: 16,
|
||||
});
|
||||
|
||||
// auto scroll when messages length changes
|
||||
const lastMessagesLength = useRef(messages.length);
|
||||
useEffect(() => {
|
||||
if (messages.length > lastMessagesLength.current && !detach) {
|
||||
scrollDomToBottom();
|
||||
}
|
||||
lastMessagesLength.current = messages.length;
|
||||
}, [messages.length, detach, scrollDomToBottom]);
|
||||
function updateWidth() {
|
||||
if (!iconRef.current || !textRef.current) return;
|
||||
const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
|
||||
const textWidth = getWidth(textRef.current);
|
||||
const iconWidth = getWidth(iconRef.current);
|
||||
setWidth({
|
||||
full: textWidth + iconWidth,
|
||||
icon: iconWidth,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
autoScroll,
|
||||
setAutoScroll,
|
||||
scrollDomToBottom,
|
||||
};
|
||||
// 使用 useLayoutEffect 确保在 DOM 更新后同步执行
|
||||
useLayoutEffect(() => {
|
||||
console.log("执行了:updateWidth");
|
||||
updateWidth();
|
||||
}, [props.text, props.icon]); // 添加依赖项,确保在 text 或 icon 变化时重新计算宽度
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles["chat-input-action-voice"], "clickable")}
|
||||
onClick={() => {
|
||||
props.onClick();
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--icon-width": `${width.icon}px`,
|
||||
"--full-width": `${width.full}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div ref={iconRef} className={styles["icon"]}>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div className={styles["text"]} ref={textRef}>
|
||||
{props.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
export function ChatActions(props: {
|
||||
uploadImage: () => void;
|
||||
@@ -503,13 +521,14 @@ export function ChatActions(props: {
|
||||
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setUserInput: (input: string) => void;
|
||||
setShowChatSidePanel: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setShowOverlay: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const config = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
const chatStore = useChatStore();
|
||||
const pluginStore = usePluginStore();
|
||||
const session = chatStore.currentSession();
|
||||
|
||||
// const showOverlayRef = useRef(showOverlay);
|
||||
// switch themes
|
||||
const theme = config.theme;
|
||||
|
||||
@@ -834,6 +853,15 @@ export function ChatActions(props: {
|
||||
)}
|
||||
{!isMobileScreen && <MCPAction />}
|
||||
</>
|
||||
|
||||
<ChatActionVoice
|
||||
onClick={() => {
|
||||
props.setShowOverlay(true);
|
||||
}}
|
||||
text="开始"
|
||||
icon={<MenuIcon />}
|
||||
/>
|
||||
|
||||
<div className={styles["chat-input-actions-end"]}>
|
||||
{config.realtimeConfig.enable && (
|
||||
<ChatAction
|
||||
@@ -986,6 +1014,47 @@ export function ShortcutKeyModal(props: { onClose: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function useScrollToBottom(
|
||||
scrollRef: RefObject<HTMLDivElement>,
|
||||
detach: boolean = false,
|
||||
messages: ChatMessage[],
|
||||
) {
|
||||
// for auto-scroll
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const scrollDomToBottom = useCallback(() => {
|
||||
const dom = scrollRef.current;
|
||||
if (dom) {
|
||||
requestAnimationFrame(() => {
|
||||
setAutoScroll(true);
|
||||
dom.scrollTo(0, dom.scrollHeight);
|
||||
});
|
||||
}
|
||||
}, [scrollRef]);
|
||||
|
||||
// auto scroll
|
||||
useEffect(() => {
|
||||
if (autoScroll && !detach) {
|
||||
scrollDomToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
// auto scroll when messages length changes
|
||||
const lastMessagesLength = useRef(messages.length);
|
||||
useEffect(() => {
|
||||
if (messages.length > lastMessagesLength.current && !detach) {
|
||||
scrollDomToBottom();
|
||||
}
|
||||
lastMessagesLength.current = messages.length;
|
||||
}, [messages.length, detach, scrollDomToBottom]);
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
autoScroll,
|
||||
setAutoScroll,
|
||||
scrollDomToBottom,
|
||||
};
|
||||
}
|
||||
|
||||
function _Chat() {
|
||||
type RenderMessage = ChatMessage & { preview?: boolean };
|
||||
|
||||
@@ -996,7 +1065,9 @@ function _Chat() {
|
||||
const fontFamily = config.fontFamily;
|
||||
|
||||
const [showExport, setShowExport] = useState(false);
|
||||
|
||||
// 使用状态来控制 InterviewOverlay 的显示和隐藏
|
||||
const [showOverlay, setShowOverlay] = useState(false);
|
||||
const showOverlayRef = useRef(showOverlay);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [userInput, setUserInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -1020,7 +1091,11 @@ function _Chat() {
|
||||
}, [scrollRef?.current?.scrollHeight]);
|
||||
|
||||
const isTyping = userInput !== "";
|
||||
|
||||
// 当子组件传回文本时更新 userInput
|
||||
const handleTextUpdate = (text: string) => {
|
||||
// console.log(`传入的文本:${text}`);
|
||||
setUserInput(text);
|
||||
};
|
||||
// if user is typing, should auto scroll to bottom
|
||||
// if user is not typing, should auto scroll to bottom only if already at bottom
|
||||
const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(
|
||||
@@ -1679,9 +1754,28 @@ function _Chat() {
|
||||
|
||||
const [showChatSidePanel, setShowChatSidePanel] = useState(false);
|
||||
|
||||
const toastShow = (text: string): void => {
|
||||
console.log(`toastShow value is: ${text}`);
|
||||
|
||||
if (text.length <= 3) {
|
||||
console.log("弹出toas啊弹啊");
|
||||
toast("没有监听到任何文本!!!", {
|
||||
icon: "⚠️", // 自定义图标
|
||||
style: {
|
||||
background: "#fff3cd", // 背景色
|
||||
color: "#856404", // 文字颜色
|
||||
border: "1px solid #ffeeba", // 边框
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
doSubmit(text);
|
||||
};
|
||||
const toastShowDebounce = debounce(toastShow, 500);
|
||||
return (
|
||||
<>
|
||||
<div className={styles.chat} key={session.id}>
|
||||
{/* 聊天窗口头部 */}
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
{isMobileScreen && (
|
||||
<div className="window-actions">
|
||||
@@ -1768,6 +1862,7 @@ function _Chat() {
|
||||
setShowModal={setShowPromptModal}
|
||||
/>
|
||||
</div>
|
||||
{/* 聊天消息区域 */}
|
||||
<div className={styles["chat-main"]}>
|
||||
<div className={styles["chat-body-container"]}>
|
||||
<div
|
||||
@@ -1781,7 +1876,6 @@ function _Chat() {
|
||||
}}
|
||||
>
|
||||
{messages
|
||||
// TODO
|
||||
// .filter((m) => !m.isMcpResponse)
|
||||
.map((message, i) => {
|
||||
const isUser = message.role === "user";
|
||||
@@ -1966,6 +2060,7 @@ function _Chat() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles["chat-message-item"]}>
|
||||
<Markdown
|
||||
key={message.streaming ? "loading" : "done"}
|
||||
@@ -2021,6 +2116,7 @@ function _Chat() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{message?.audio_url && (
|
||||
<div className={styles["chat-message-audio"]}>
|
||||
<audio src={message.audio_url} controls />
|
||||
@@ -2039,6 +2135,7 @@ function _Chat() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* 消息输入区域 */}
|
||||
<div className={styles["chat-input-panel"]}>
|
||||
<PromptHints
|
||||
prompts={promptHints}
|
||||
@@ -2067,6 +2164,7 @@ function _Chat() {
|
||||
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
||||
setUserInput={setUserInput}
|
||||
setShowChatSidePanel={setShowChatSidePanel}
|
||||
setShowOverlay={setShowOverlay}
|
||||
/>
|
||||
<label
|
||||
className={clsx(styles["chat-input-panel-inner"], {
|
||||
@@ -2126,6 +2224,8 @@ function _Chat() {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 侧边面板 */}
|
||||
<div
|
||||
className={clsx(styles["chat-side-panel"], {
|
||||
[styles["mobile"]]: isMobileScreen,
|
||||
@@ -2160,6 +2260,28 @@ function _Chat() {
|
||||
{showShortcutKeyModal && (
|
||||
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
|
||||
)}
|
||||
{/* 当状态为 true 时,加载 InterviewOverlay 组件 */}
|
||||
{showOverlay && (
|
||||
<InterviewOverlay
|
||||
onClose={() => {
|
||||
setShowOverlay(false);
|
||||
}}
|
||||
onTextUpdate={handleTextUpdate}
|
||||
submitMessage={toastShowDebounce}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 全局的 Toaster 组件,可以设置默认位置和样式 */}
|
||||
<Toaster
|
||||
position="top-center"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: "#fff3cd",
|
||||
color: "#856404",
|
||||
border: "1px solid #ffeeba",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
@mixin container {
|
||||
background-color: var(--white);
|
||||
border: var(--border-in-light);
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
// 移除边框
|
||||
// border: var(--border-in-light);
|
||||
// 移除圆角
|
||||
// border-radius: 20px;
|
||||
// 移除阴影
|
||||
// box-shadow: var(--shadow);
|
||||
color: var(--black);
|
||||
background-color: var(--white);
|
||||
min-width: 600px;
|
||||
min-height: 370px;
|
||||
max-width: 1200px;
|
||||
|
||||
// 修改最小宽度和高度
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
width: var(--window-width);
|
||||
height: var(--window-height);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -24,13 +30,13 @@
|
||||
@media only screen and (min-width: 600px) {
|
||||
.tight-container {
|
||||
--window-width: 100vw;
|
||||
--window-height: var(--full-height);
|
||||
--window-height: 100vh;
|
||||
--window-content-width: calc(100% - var(--sidebar-width));
|
||||
|
||||
@include container();
|
||||
|
||||
max-width: 100vw;
|
||||
max-height: var(--full-height);
|
||||
max-height: 100vh;
|
||||
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
@@ -107,10 +113,10 @@
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.container {
|
||||
min-height: unset;
|
||||
min-width: unset;
|
||||
max-height: unset;
|
||||
min-width: unset;
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
max-height: 100vh;
|
||||
max-width: 100vw;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,11 @@ const McpMarketPage = dynamic(
|
||||
},
|
||||
);
|
||||
|
||||
// const InterviewPage = dynamic(
|
||||
// async ()=>(await import("./interview-overlay")).InterviewOverlay,{
|
||||
// loading: ()=> <Loading noLogo/>
|
||||
// }
|
||||
// )
|
||||
export function useSwitchTheme() {
|
||||
const config = useAppConfig();
|
||||
|
||||
@@ -202,6 +207,7 @@ function Screen() {
|
||||
<Route path={Path.Chat} element={<Chat />} />
|
||||
<Route path={Path.Settings} element={<Settings />} />
|
||||
<Route path={Path.McpMarket} element={<McpMarketPage />} />
|
||||
{/* <Route path={Path.Interview} element={<InterviewPage/>}/> */}
|
||||
</Routes>
|
||||
</WindowContent>
|
||||
</>
|
||||
|
||||
397
app/components/interview-overlay.tsx
Normal file
397
app/components/interview-overlay.tsx
Normal file
@@ -0,0 +1,397 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import StopIcon from "../icons/pause.svg";
|
||||
import SpeechRecognition, {
|
||||
useSpeechRecognition,
|
||||
} from "react-speech-recognition";
|
||||
|
||||
interface InterviewOverlayProps {
|
||||
onClose: () => void;
|
||||
onTextUpdate: (text: string) => void;
|
||||
submitMessage: (text: string) => void;
|
||||
}
|
||||
|
||||
export const InterviewOverlay: React.FC<InterviewOverlayProps> = ({
|
||||
onClose,
|
||||
onTextUpdate,
|
||||
submitMessage,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [countdown, setCountdown] = useState(20);
|
||||
const countdownRef = useRef(countdown);
|
||||
const intervalIdRef = useRef<NodeJS.Timeout | null>(null);
|
||||
// 添加暂停状态
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
// 使用 react-speech-recognition 的钩子
|
||||
const {
|
||||
transcript,
|
||||
listening,
|
||||
resetTranscript,
|
||||
browserSupportsSpeechRecognition,
|
||||
isMicrophoneAvailable,
|
||||
} = useSpeechRecognition();
|
||||
|
||||
// 保存当前文本的引用,用于在倒计时结束时提交
|
||||
const transcriptRef = useRef(transcript);
|
||||
|
||||
useEffect(() => {
|
||||
transcriptRef.current = transcript;
|
||||
onTextUpdate(transcript);
|
||||
|
||||
// 当有新的语音识别结果时,重置倒计时
|
||||
if (transcript) {
|
||||
setCountdown(20);
|
||||
countdownRef.current = 20;
|
||||
}
|
||||
}, [transcript, onTextUpdate]);
|
||||
|
||||
// 检查浏览器是否支持语音识别
|
||||
useEffect(() => {
|
||||
if (!browserSupportsSpeechRecognition) {
|
||||
console.error("您的浏览器不支持语音识别功能");
|
||||
} else if (!isMicrophoneAvailable) {
|
||||
console.error("无法访问麦克风");
|
||||
}
|
||||
}, [browserSupportsSpeechRecognition, isMicrophoneAvailable]);
|
||||
|
||||
// 开始语音识别
|
||||
useEffect(() => {
|
||||
if (visible && !isPaused) {
|
||||
// 配置语音识别
|
||||
SpeechRecognition.startListening({
|
||||
continuous: true,
|
||||
language: "zh-CN",
|
||||
});
|
||||
|
||||
// 设置倒计时
|
||||
intervalIdRef.current = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
const newCount = prev - 1;
|
||||
countdownRef.current = newCount;
|
||||
|
||||
if (newCount <= 0) {
|
||||
stopRecognition();
|
||||
}
|
||||
return newCount;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalIdRef.current) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
}
|
||||
SpeechRecognition.stopListening();
|
||||
};
|
||||
}, [visible, isPaused]);
|
||||
|
||||
const stopRecognition = () => {
|
||||
try {
|
||||
SpeechRecognition.stopListening();
|
||||
|
||||
// 提交最终结果
|
||||
if (transcriptRef.current) {
|
||||
submitMessage(transcriptRef.current);
|
||||
}
|
||||
|
||||
// 清理倒计时
|
||||
if (intervalIdRef.current) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
}
|
||||
|
||||
// 关闭overlay
|
||||
setVisible(false);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("停止语音识别失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加暂停/恢复功能
|
||||
const togglePause = () => {
|
||||
if (!isPaused) {
|
||||
// 暂停
|
||||
SpeechRecognition.stopListening();
|
||||
if (intervalIdRef.current) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
}
|
||||
|
||||
// 提交当前文本
|
||||
if (transcriptRef.current) {
|
||||
submitMessage(transcriptRef.current);
|
||||
resetTranscript();
|
||||
}
|
||||
} else {
|
||||
// 恢复
|
||||
console.log("recover ");
|
||||
|
||||
// 先确保停止当前可能存在的监听
|
||||
SpeechRecognition.abortListening();
|
||||
|
||||
// 短暂延迟后重新启动监听
|
||||
setTimeout(() => {
|
||||
SpeechRecognition.startListening({
|
||||
continuous: true,
|
||||
language: "zh-CN",
|
||||
});
|
||||
|
||||
// 重置文本
|
||||
resetTranscript();
|
||||
}, 100);
|
||||
// 重新设置倒计时
|
||||
intervalIdRef.current = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
const newCount = prev - 1;
|
||||
countdownRef.current = newCount;
|
||||
|
||||
if (newCount <= 0) {
|
||||
stopRecognition();
|
||||
}
|
||||
return newCount;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
setIsPaused(!isPaused);
|
||||
};
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: "20px",
|
||||
right: "20px",
|
||||
width: "33vw",
|
||||
height: "100vh",
|
||||
// maxHeight: "80vh",
|
||||
backgroundColor: "#1e1e1e", // 替换 var(--gray)
|
||||
border: "1px solid rgba(255, 255, 255, 0.2)", // 替换 var(--border-in-light)
|
||||
borderRadius: "10px",
|
||||
boxShadow: "0 5px 20px rgba(0, 0, 0, 0.3)", // 替换 var(--shadow)
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
color: "#ffffff", // 替换 C 为白色
|
||||
zIndex: 1000,
|
||||
padding: "20px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "1.5rem",
|
||||
fontWeight: "500",
|
||||
marginBottom: "1rem",
|
||||
textAlign: "left",
|
||||
color: "#ffffff", // 替换 var(--white)
|
||||
}}
|
||||
>
|
||||
剩余{" "}
|
||||
<span
|
||||
style={{
|
||||
color: countdown <= 5 ? "#ff6b6b" : "#4caf50",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{countdown}
|
||||
</span>{" "}
|
||||
秒,超时将自动发送
|
||||
</h2>
|
||||
|
||||
{/* 语音识别状态指示器 */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
marginBottom: "1rem",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)", // 替换 var(--black-50)
|
||||
padding: "0.5rem 1rem",
|
||||
borderRadius: "1rem",
|
||||
width: "fit-content",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: listening ? "#4caf50" : "#ff6b6b",
|
||||
marginRight: "10px",
|
||||
boxShadow: listening ? "0 0 10px #4caf50" : "none",
|
||||
animation: listening ? "pulse 1.5s infinite" : "none",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: "0.9rem" }}>
|
||||
{listening ? "正在监听..." : isPaused ? "已暂停" : "未监听"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{(!browserSupportsSpeechRecognition || !isMicrophoneAvailable) && (
|
||||
<div
|
||||
style={{
|
||||
color: "#ff6b6b",
|
||||
marginBottom: "1rem",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)", // 替换 var(--black-50)
|
||||
padding: "0.75rem 1rem",
|
||||
borderRadius: "0.5rem",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{!browserSupportsSpeechRecognition
|
||||
? "您的浏览器不支持语音识别功能,请使用Chrome浏览器"
|
||||
: "无法访问麦克风,请检查麦克风权限"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 识别文本显示区域 */}
|
||||
{transcript && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
marginBottom: "1rem",
|
||||
padding: "1rem",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)", // 替换 var(--black-50)
|
||||
borderRadius: "0.5rem",
|
||||
maxHeight: "120px",
|
||||
overflowY: "auto",
|
||||
textAlign: "left",
|
||||
fontSize: "0.9rem",
|
||||
lineHeight: "1.5",
|
||||
border: "1px solid rgba(0, 0, 0, 0.5)", // 替换 var(--black-50)
|
||||
}}
|
||||
>
|
||||
{transcript}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 按钮区域 */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: "0.5rem",
|
||||
marginTop: "1rem",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{/* 暂停/恢复按钮 */}
|
||||
<button
|
||||
onClick={togglePause}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "0.5rem",
|
||||
backgroundColor: isPaused ? "#4caf50" : "#ff9800",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "0.9rem",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
flex: "1",
|
||||
}}
|
||||
onMouseOver={(e) =>
|
||||
(e.currentTarget.style.backgroundColor = isPaused
|
||||
? "#45a049"
|
||||
: "#f57c00")
|
||||
}
|
||||
onMouseOut={(e) =>
|
||||
(e.currentTarget.style.backgroundColor = isPaused
|
||||
? "#4caf50"
|
||||
: "#ff9800")
|
||||
}
|
||||
>
|
||||
<span>{isPaused ? "▶️ 恢复监听" : "⏸️ 暂停并发送"}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={stopRecognition}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "0.5rem",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)", // 替换 var(--black-50)
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "0.9rem",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
flex: "1",
|
||||
}}
|
||||
onMouseOver={
|
||||
(e) => (e.currentTarget.style.backgroundColor = "#000000") // 替换 var(--black)
|
||||
}
|
||||
onMouseOut={
|
||||
(e) =>
|
||||
(e.currentTarget.style.backgroundColor = "rgba(0, 0, 0, 0.5)") // 替换 var(--black-50)
|
||||
}
|
||||
>
|
||||
<StopIcon />
|
||||
<span>停止并发送</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={resetTranscript}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "0.5rem",
|
||||
backgroundColor: "transparent",
|
||||
color: "white",
|
||||
border: "1px solid rgba(0, 0, 0, 0.5)", // 替换 var(--black-50)
|
||||
borderRadius: "0.5rem",
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "0.9rem",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease",
|
||||
flex: "1",
|
||||
}}
|
||||
onMouseOver={
|
||||
(e) =>
|
||||
(e.currentTarget.style.backgroundColor = "rgba(0, 0, 0, 0.5)") // 替换 var(--black-50)
|
||||
}
|
||||
onMouseOut={(e) =>
|
||||
(e.currentTarget.style.backgroundColor = "transparent")
|
||||
}
|
||||
>
|
||||
<span>🗑️ 清空</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 添加脉冲动画 */}
|
||||
<style>
|
||||
{`
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,6 @@ import GithubIcon from "../icons/github.svg";
|
||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import MaskIcon from "../icons/mask.svg";
|
||||
import McpIcon from "../icons/mcp.svg";
|
||||
import DragIcon from "../icons/drag.svg";
|
||||
import DiscoveryIcon from "../icons/discovery.svg";
|
||||
@@ -256,7 +255,7 @@ export function SideBar(props: { className?: string }) {
|
||||
shouldNarrow={shouldNarrow}
|
||||
>
|
||||
<div className={styles["sidebar-header-bar"]}>
|
||||
<IconButton
|
||||
{/* <IconButton
|
||||
icon={<MaskIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
@@ -268,7 +267,7 @@ export function SideBar(props: { className?: string }) {
|
||||
}
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
/> */}
|
||||
{mcpEnabled && (
|
||||
<IconButton
|
||||
icon={<McpIcon />}
|
||||
|
||||
@@ -52,6 +52,7 @@ export enum Path {
|
||||
Artifacts = "/artifacts",
|
||||
SearchChat = "/search-chat",
|
||||
McpMarket = "/mcp-market",
|
||||
Interview = "/interview",
|
||||
}
|
||||
|
||||
export enum ApiPath {
|
||||
@@ -652,15 +653,15 @@ const siliconflowModels = [
|
||||
|
||||
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||
export const DEFAULT_MODELS = [
|
||||
...openaiModels.map((name) => ({
|
||||
...siliconflowModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++, // Global sequence sort(index)
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
sorted: 1, // 这里是固定的,确保顺序与之前内置的版本一致
|
||||
id: "siliconflow",
|
||||
providerName: "SiliconFlow",
|
||||
providerType: "siliconflow",
|
||||
sorted: 1,
|
||||
},
|
||||
})),
|
||||
...openaiModels.map((name) => ({
|
||||
@@ -795,15 +796,15 @@ export const DEFAULT_MODELS = [
|
||||
sorted: 13,
|
||||
},
|
||||
})),
|
||||
...siliconflowModels.map((name) => ({
|
||||
...openaiModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
sorted: seq++, // Global sequence sort(index)
|
||||
provider: {
|
||||
id: "siliconflow",
|
||||
providerName: "SiliconFlow",
|
||||
providerType: "siliconflow",
|
||||
sorted: 14,
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
sorted: 14, // 这里是固定的,确保顺序与之前内置的版本一致
|
||||
},
|
||||
})),
|
||||
] as const;
|
||||
|
||||
@@ -77,9 +77,9 @@ export const ALL_LANG_OPTIONS: Record<Lang, string> = {
|
||||
};
|
||||
|
||||
const LANG_KEY = "lang";
|
||||
const DEFAULT_LANG = "en";
|
||||
const DEFAULT_LANG = "cn";
|
||||
|
||||
const fallbackLang = en;
|
||||
const fallbackLang = cn;
|
||||
const targetLang = ALL_LANGS[getLang()] as LocaleType;
|
||||
|
||||
// if target lang missing some fields, it will use fallback lang string
|
||||
|
||||
@@ -45,7 +45,7 @@ export const DEFAULT_CONFIG = {
|
||||
avatar: "1f603",
|
||||
fontSize: 14,
|
||||
fontFamily: "",
|
||||
theme: Theme.Auto as Theme,
|
||||
theme: Theme.Dark as Theme,
|
||||
tightBorder: !!config?.isApp,
|
||||
sendPreviewBubble: true,
|
||||
enableAutoGenerateTitle: true,
|
||||
|
||||
0
app/utils/voice-start.ts
Normal file
0
app/utils/voice-start.ts
Normal file
Reference in New Issue
Block a user