part function about interview

This commit is contained in:
Nan
2025-04-25 10:23:54 +08:00
parent b6f5d75656
commit e9c9d90902
24 changed files with 20416 additions and 2785 deletions

View File

@@ -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 {

View File

@@ -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",
},
}}
/>
</>
);
}

View File

@@ -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;
}

View File

@@ -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>
</>

View 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>
);
};

View File

@@ -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 />}

View File

@@ -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;

View File

@@ -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

View File

@@ -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
View File