Merge remote-tracking branch 'origin/main' into feature/aarch64

This commit is contained in:
lloydzhou 2024-09-13 17:40:17 +08:00
commit f2aeb83e4e
11 changed files with 255 additions and 15 deletions

View File

@ -160,6 +160,7 @@ export class ChatGPTApi implements LLMApi {
let requestPayload: RequestPayload | DalleRequestPayload; let requestPayload: RequestPayload | DalleRequestPayload;
const isDalle3 = _isDalle3(options.config.model); const isDalle3 = _isDalle3(options.config.model);
const isO1 = options.config.model.startsWith("o1");
if (isDalle3) { if (isDalle3) {
const prompt = getMessageTextContent( const prompt = getMessageTextContent(
options.messages.slice(-1)?.pop() as any, options.messages.slice(-1)?.pop() as any,
@ -181,30 +182,32 @@ export class ChatGPTApi implements LLMApi {
const content = visionModel const content = visionModel
? await preProcessImageContent(v.content) ? await preProcessImageContent(v.content)
: getMessageTextContent(v); : getMessageTextContent(v);
if (!(isO1 && v.role === "system"))
messages.push({ role: v.role, content }); messages.push({ role: v.role, content });
} }
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
requestPayload = { requestPayload = {
messages, messages,
stream: options.config.stream, stream: !isO1 ? options.config.stream : false,
model: modelConfig.model, model: modelConfig.model,
temperature: modelConfig.temperature, temperature: !isO1 ? modelConfig.temperature : 1,
presence_penalty: modelConfig.presence_penalty, presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
frequency_penalty: modelConfig.frequency_penalty, frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
top_p: modelConfig.top_p, top_p: !isO1 ? modelConfig.top_p : 1,
// max_tokens: Math.max(modelConfig.max_tokens, 1024), // max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
}; };
// add max_tokens to vision model // add max_tokens to vision model
if (visionModel && modelConfig.model.includes("preview")) { if (visionModel) {
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
} }
} }
console.log("[Request] openai payload: ", requestPayload); console.log("[Request] openai payload: ", requestPayload);
const shouldStream = !isDalle3 && !!options.config.stream; const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
const controller = new AbortController(); const controller = new AbortController();
options.onController?.(controller); options.onController?.(controller);
@ -313,7 +316,7 @@ export class ChatGPTApi implements LLMApi {
// make a fetch request // make a fetch request
const requestTimeoutId = setTimeout( const requestTimeoutId = setTimeout(
() => controller.abort(), () => controller.abort(),
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
); );
const res = await fetch(chatPath, chatPayload); const res = await fetch(chatPath, chatPayload);

View File

@ -80,7 +80,7 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
}, [props.autoHeight, props.height, iframeHeight]); }, [props.autoHeight, props.height, iframeHeight]);
const srcDoc = useMemo(() => { const srcDoc = useMemo(() => {
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`; const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
if (props.code.includes("<!DOCTYPE html>")) { if (props.code.includes("<!DOCTYPE html>")) {
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script); props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
} }

View File

@ -646,3 +646,51 @@
bottom: 30px; bottom: 30px;
} }
} }
.shortcut-key-container {
padding: 10px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.shortcut-key-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 16px;
}
.shortcut-key-item {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
padding: 10px;
background-color: var(--white);
}
.shortcut-key-title {
font-size: 14px;
color: var(--black);
}
.shortcut-key-keys {
display: flex;
gap: 8px;
}
.shortcut-key {
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
border-radius: 8px;
padding: 4px;
background-color: var(--gray);
min-width: 32px;
}
.shortcut-key span {
font-size: 12px;
color: var(--black);
}

View File

@ -42,6 +42,7 @@ import SizeIcon from "../icons/size.svg";
import QualityIcon from "../icons/hd.svg"; import QualityIcon from "../icons/hd.svg";
import StyleIcon from "../icons/palette.svg"; import StyleIcon from "../icons/palette.svg";
import PluginIcon from "../icons/plugin.svg"; import PluginIcon from "../icons/plugin.svg";
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
import { import {
ChatMessage, ChatMessage,
@ -191,7 +192,7 @@ function PromptToast(props: {
return ( return (
<div className={styles["prompt-toast"]} key="prompt-toast"> <div className={styles["prompt-toast"]} key="prompt-toast">
{props.showToast && ( {props.showToast && context.length > 0 && (
<div <div
className={styles["prompt-toast-inner"] + " clickable"} className={styles["prompt-toast-inner"] + " clickable"}
role="button" role="button"
@ -440,6 +441,7 @@ export function ChatActions(props: {
showPromptHints: () => void; showPromptHints: () => void;
hitBottom: boolean; hitBottom: boolean;
uploading: boolean; uploading: boolean;
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
}) { }) {
const config = useAppConfig(); const config = useAppConfig();
const navigate = useNavigate(); const navigate = useNavigate();
@ -505,6 +507,8 @@ export function ChatActions(props: {
const currentStyle = const currentStyle =
chatStore.currentSession().mask.modelConfig?.style ?? "vivid"; chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
const isMobileScreen = useMobileScreen();
useEffect(() => { useEffect(() => {
const show = isVisionModel(currentModel); const show = isVisionModel(currentModel);
setShowUploadImage(show); setShowUploadImage(show);
@ -758,6 +762,14 @@ export function ChatActions(props: {
}} }}
/> />
)} )}
{!isMobileScreen && (
<ChatAction
onClick={() => props.setShowShortcutKeyModal(true)}
text={Locale.Chat.ShortcutKey.Title}
icon={<ShortcutkeyIcon />}
/>
)}
</div> </div>
); );
} }
@ -832,6 +844,67 @@ export function DeleteImageButton(props: { deleteImage: () => void }) {
); );
} }
export function ShortcutKeyModal(props: { onClose: () => void }) {
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const shortcuts = [
{
title: Locale.Chat.ShortcutKey.newChat,
keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
},
{ title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
{
title: Locale.Chat.ShortcutKey.copyLastCode,
keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
},
{
title: Locale.Chat.ShortcutKey.copyLastMessage,
keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
},
{
title: Locale.Chat.ShortcutKey.showShortcutKey,
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
},
];
return (
<div className="modal-mask">
<Modal
title={Locale.Chat.ShortcutKey.Title}
onClose={props.onClose}
actions={[
<IconButton
type="primary"
text={Locale.UI.Confirm}
icon={<ConfirmIcon />}
key="ok"
onClick={() => {
props.onClose();
}}
/>,
]}
>
<div className={styles["shortcut-key-container"]}>
<div className={styles["shortcut-key-grid"]}>
{shortcuts.map((shortcut, index) => (
<div key={index} className={styles["shortcut-key-item"]}>
<div className={styles["shortcut-key-title"]}>
{shortcut.title}
</div>
<div className={styles["shortcut-key-keys"]}>
{shortcut.keys.map((key, i) => (
<div key={i} className={styles["shortcut-key"]}>
<span>{key}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
</Modal>
</div>
);
}
function _Chat() { function _Chat() {
type RenderMessage = ChatMessage & { preview?: boolean }; type RenderMessage = ChatMessage & { preview?: boolean };
@ -1376,6 +1449,70 @@ function _Chat() {
setAttachImages(images); setAttachImages(images);
} }
// 快捷键 shortcut keys
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
useEffect(() => {
const handleKeyDown = (event: any) => {
// 打开新聊天 command + shift + o
if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.key.toLowerCase() === "o"
) {
event.preventDefault();
setTimeout(() => {
chatStore.newSession();
navigate(Path.Chat);
}, 10);
}
// 聚焦聊天输入 shift + esc
else if (event.shiftKey && event.key.toLowerCase() === "escape") {
event.preventDefault();
inputRef.current?.focus();
}
// 复制最后一个代码块 command + shift + ;
else if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.code === "Semicolon"
) {
event.preventDefault();
const copyCodeButton =
document.querySelectorAll<HTMLElement>(".copy-code-button");
if (copyCodeButton.length > 0) {
copyCodeButton[copyCodeButton.length - 1].click();
}
}
// 复制最后一个回复 command + shift + c
else if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.key.toLowerCase() === "c"
) {
event.preventDefault();
const lastNonUserMessage = messages
.filter((message) => message.role !== "user")
.pop();
if (lastNonUserMessage) {
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
copyToClipboard(lastMessageContent);
}
}
// 展示快捷键 command + /
else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
event.preventDefault();
setShowShortcutKeyModal(true);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [messages, chatStore, navigate]);
return ( return (
<div className={styles.chat} key={session.id}> <div className={styles.chat} key={session.id}>
<div className="window-header" data-tauri-drag-region> <div className="window-header" data-tauri-drag-region>
@ -1692,6 +1829,7 @@ function _Chat() {
setUserInput("/"); setUserInput("/");
onSearch(""); onSearch("");
}} }}
setShowShortcutKeyModal={setShowShortcutKeyModal}
/> />
<label <label
className={`${styles["chat-input-panel-inner"]} ${ className={`${styles["chat-input-panel-inner"]} ${
@ -1763,6 +1901,10 @@ function _Chat() {
}} }}
/> />
)} )}
{showShortcutKeyModal && (
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
)}
</div> </div>
); );
} }

View File

@ -237,9 +237,26 @@ function escapeBrackets(text: string) {
); );
} }
function tryWrapHtmlCode(text: string) {
// try add wrap html code (fixed: html codeblock include 2 newline)
return text
.replace(
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
(match, quoteStart, lang, newLine, doctype) => {
return !quoteStart ? "\n```html\n" + doctype : match;
},
)
.replace(
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
},
);
}
function _MarkDownContent(props: { content: string }) { function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => { const escapedContent = useMemo(() => {
return escapeBrackets(escapeDollarNumber(props.content)); return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
}, [props.content]); }, [props.content]);
return ( return (

View File

@ -250,6 +250,8 @@ export const KnowledgeCutOffDate: Record<string, string> = {
"gpt-4o-mini": "2023-10", "gpt-4o-mini": "2023-10",
"gpt-4o-mini-2024-07-18": "2023-10", "gpt-4o-mini-2024-07-18": "2023-10",
"gpt-4-vision-preview": "2023-04", "gpt-4-vision-preview": "2023-04",
"o1-mini": "2023-10",
"o1-preview": "2023-10",
// After improvements, // After improvements,
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously. // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
"gemini-pro": "2023-12", "gemini-pro": "2023-12",
@ -276,6 +278,8 @@ const openaiModels = [
"gpt-4-turbo-2024-04-09", "gpt-4-turbo-2024-04-09",
"gpt-4-1106-preview", "gpt-4-1106-preview",
"dall-e-3", "dall-e-3",
"o1-mini",
"o1-preview"
]; ];
const googleModels = [ const googleModels = [

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V37C4 38.1046 4.89543 39 6 39H42C43.1046 39 44 38.1046 44 37V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"/><path d="M12 19H14" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 19H23" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M29 19H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 28H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 734 B

View File

@ -1,3 +1,4 @@
import { ShortcutKeyModal } from "../components/chat";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
@ -81,6 +82,14 @@ const cn = {
SaveAs: "存为面具", SaveAs: "存为面具",
}, },
IsContext: "预设提示词", IsContext: "预设提示词",
ShortcutKey: {
Title: "键盘快捷方式",
newChat: "打开新聊天",
focusInput: "聚焦输入框",
copyLastMessage: "复制最后一个回复",
copyLastCode: "复制最后一个代码块",
showShortcutKey: "显示快捷方式",
},
}, },
Export: { Export: {
Title: "分享聊天记录", Title: "分享聊天记录",
@ -495,8 +504,8 @@ const cn = {
}, },
}, },
Copy: { Copy: {
Success: "已写入剪板", Success: "已写入剪板",
Failed: "复制失败,请赋予剪板权限", Failed: "复制失败,请赋予剪板权限",
}, },
Download: { Download: {
Success: "内容已下载到您的目录。", Success: "内容已下载到您的目录。",

View File

@ -83,6 +83,14 @@ const en: LocaleType = {
SaveAs: "Save as Mask", SaveAs: "Save as Mask",
}, },
IsContext: "Contextual Prompt", IsContext: "Contextual Prompt",
ShortcutKey: {
Title: "Keyboard Shortcuts",
newChat: "Open New Chat",
focusInput: "Focus Input Field",
copyLastMessage: "Copy Last Reply",
copyLastCode: "Copy Last Code Block",
showShortcutKey: "Show Shortcuts",
},
}, },
Export: { Export: {
Title: "Export Messages", Title: "Export Messages",

View File

@ -81,6 +81,14 @@ const tw = {
SaveAs: "另存新檔", SaveAs: "另存新檔",
}, },
IsContext: "預設提示詞", IsContext: "預設提示詞",
ShortcutKey: {
Title: "鍵盤快捷方式",
newChat: "打開新聊天",
focusInput: "聚焦輸入框",
copyLastMessage: "複製最後一個回覆",
copyLastCode: "複製最後一個代碼塊",
showShortcutKey: "顯示快捷方式",
},
}, },
Export: { Export: {
Title: "將聊天記錄匯出為 Markdown", Title: "將聊天記錄匯出為 Markdown",

View File

@ -9,7 +9,7 @@
}, },
"package": { "package": {
"productName": "NextChat", "productName": "NextChat",
"version": "2.15.1" "version": "2.15.2"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {