diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index d1b9ab3fd..16f70dbd6 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -161,6 +161,7 @@ export class ChatGPTApi implements LLMApi { let requestPayload: RequestPayload | DalleRequestPayload; const isDalle3 = _isDalle3(options.config.model); + const isO1 = options.config.model.startsWith("o1"); if (isDalle3) { const prompt = getMessageTextContent( options.messages.slice(-1)?.pop() as any, @@ -182,30 +183,32 @@ export class ChatGPTApi implements LLMApi { const content = visionModel ? await preProcessImageContent(v.content) : getMessageTextContent(v); - messages.push({ role: v.role, content }); + if (!(isO1 && v.role === "system")) + 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 = { messages, - stream: options.config.stream, + stream: !isO1 ? options.config.stream : false, model: modelConfig.model, - temperature: modelConfig.temperature, - presence_penalty: modelConfig.presence_penalty, - frequency_penalty: modelConfig.frequency_penalty, - top_p: modelConfig.top_p, + temperature: !isO1 ? modelConfig.temperature : 1, + presence_penalty: !isO1 ? modelConfig.presence_penalty : 0, + frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0, + top_p: !isO1 ? modelConfig.top_p : 1, // 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. }; // add max_tokens to vision model - if (visionModel && modelConfig.model.includes("preview")) { + if (visionModel) { requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); } } console.log("[Request] openai payload: ", requestPayload); - const shouldStream = !isDalle3 && !!options.config.stream; + const shouldStream = !isDalle3 && !!options.config.stream && !isO1; const controller = new AbortController(); options.onController?.(controller); @@ -326,7 +329,7 @@ export class ChatGPTApi implements LLMApi { // make a fetch request const requestTimeoutId = setTimeout( () => 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); diff --git a/app/components/artifacts.tsx b/app/components/artifacts.tsx index ac0d713b3..d725ee659 100644 --- a/app/components/artifacts.tsx +++ b/app/components/artifacts.tsx @@ -80,7 +80,7 @@ export const HTMLPreview = forwardRef( }, [props.autoHeight, props.height, iframeHeight]); const srcDoc = useMemo(() => { - const script = ``; + const script = ``; if (props.code.includes("")) { props.code.replace("", "" + script); } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 345b22360..975a64498 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -204,7 +204,7 @@ function PromptToast(props: { return (
- {props.showToast && ( + {props.showToast && context.length > 0 && (
{ const show = isVisionModel(currentModel); setShowUploadImage(show); @@ -648,7 +650,7 @@ export function ChatActions(props: { items={models.map((m) => ({ title: `${m.displayName}${ m?.provider?.providerName - ? "(" + m?.provider?.providerName + ")" + ? " (" + m?.provider?.providerName + ")" : "" }`, subTitle: m.describe, @@ -788,11 +790,13 @@ export function ChatActions(props: { /> )} - props.setShowShortcutKeyModal(true)} - text={Locale.Chat.ShortcutKey.Title} - icon={} - /> + {!isMobileScreen && ( + props.setShowShortcutKeyModal(true)} + text={Locale.Chat.ShortcutKey.Title} + icon={} + /> + )}
); } diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index eb5a25157..e871f8b42 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -238,9 +238,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]*?)()/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 }) { const escapedContent = useMemo(() => { - return escapeBrackets(escapeDollarNumber(props.content)); + return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content))); }, [props.content]); return ( diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 6ce25f664..04cd3ff01 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -5,13 +5,19 @@ import Locale from "../locales"; import { InputRange } from "./input-range"; import { ListItem, Select } from "./ui-lib"; import { useAllModels } from "../utils/hooks"; +import { groupBy } from "lodash-es"; export function ModelConfigList(props: { modelConfig: ModelConfig; updateConfig: (updater: (config: ModelConfig) => void) => void; }) { const allModels = useAllModels(); + const groupModels = groupBy( + allModels.filter((v) => v.available), + "provider.providerName", + ); const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; + const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`; return ( <> @@ -19,6 +25,7 @@ export function ModelConfigList(props: { + + + ); } diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index bb1433ffb..20b8ec89a 100644 --- a/app/components/ui-lib.module.scss +++ b/app/components/ui-lib.module.scss @@ -252,6 +252,12 @@ position: relative; max-width: fit-content; + &.left-align-option { + option { + text-align: left; + } + } + .select-with-icon-select { height: 100%; border: var(--border-in-light); diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx index f0c59d7d2..31f8fe0fe 100644 --- a/app/components/ui-lib.tsx +++ b/app/components/ui-lib.tsx @@ -313,13 +313,19 @@ export function PasswordInput( export function Select( props: React.DetailedHTMLProps< - React.SelectHTMLAttributes, + React.SelectHTMLAttributes & { + align?: "left" | "center"; + }, HTMLSelectElement >, ) { - const { className, children, ...otherProps } = props; + const { className, children, align, ...otherProps } = props; return ( -
+
diff --git a/app/constant.ts b/app/constant.ts index c9aa264c6..0e6cd731f 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -252,6 +252,8 @@ export const KnowledgeCutOffDate: Record = { "gpt-4o-mini": "2023-10", "gpt-4o-mini-2024-07-18": "2023-10", "gpt-4-vision-preview": "2023-04", + "o1-mini": "2023-10", + "o1-preview": "2023-10", // After improvements, // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously. "gemini-pro": "2023-12", @@ -278,6 +280,8 @@ const openaiModels = [ "gpt-4-turbo-2024-04-09", "gpt-4-1106-preview", "dall-e-3", + "o1-mini", + "o1-preview", ]; const googleModels = [ diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 4b18e2e75..9deff9700 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -515,6 +515,10 @@ const cn = { }, Model: "模型 (model)", + CompressModel: { + Title: "压缩模型", + SubTitle: "用于压缩历史记录的模型", + }, Temperature: { Title: "随机性 (temperature)", SubTitle: "值越大,回复越随机", @@ -550,8 +554,8 @@ const cn = { }, }, Copy: { - Success: "已写入剪切板", - Failed: "复制失败,请赋予剪切板权限", + Success: "已写入剪贴板", + Failed: "复制失败,请赋予剪贴板权限", }, Download: { Success: "内容已下载到您的目录。", diff --git a/app/locales/en.ts b/app/locales/en.ts index b84065461..a5758ebb1 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -519,6 +519,10 @@ const en: LocaleType = { }, Model: "Model", + CompressModel: { + Title: "Compression Model", + SubTitle: "Model used to compress history", + }, Temperature: { Title: "Temperature", SubTitle: "A larger value makes the more random output", diff --git a/app/locales/tw.ts b/app/locales/tw.ts index c54a7b8c5..88b86772c 100644 --- a/app/locales/tw.ts +++ b/app/locales/tw.ts @@ -368,6 +368,10 @@ const tw = { }, Model: "模型 (model)", + CompressModel: { + Title: "壓縮模型", + SubTitle: "用於壓縮歷史記錄的模型", + }, Temperature: { Title: "隨機性 (temperature)", SubTitle: "值越大,回應越隨機", diff --git a/app/store/chat.ts b/app/store/chat.ts index 0a164ed0d..b10d12548 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -1,9 +1,15 @@ -import { trimTopic, getMessageTextContent } from "../utils"; +import { getMessageTextContent, trimTopic } from "../utils"; -import Locale, { getLang } from "../locales"; +import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; +import { nanoid } from "nanoid"; +import type { + ClientApi, + MultimodalContent, + RequestMessage, +} from "../client/api"; +import { getClientApi } from "../client/api"; +import { ChatControllerPool } from "../client/controller"; import { showToast } from "../components/ui-lib"; -import { ModelConfig, ModelType, useAppConfig } from "./config"; -import { createEmptyMask, Mask } from "./mask"; import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, @@ -11,9 +17,9 @@ import { KnowledgeCutOffDate, ServiceProvider, StoreKey, - SUMMARIZE_MODEL, - GEMINI_SUMMARIZE_MODEL, } from "../constant"; +import Locale, { getLang } from "../locales"; +import { isDalle3, safeLocalStorage } from "../utils"; import { getClientApi, getHeaders, @@ -26,13 +32,10 @@ import type { } from "../client/api"; import { ChatControllerPool } from "../client/controller"; import { prettyObject } from "../utils/format"; -import { estimateTokenLength } from "../utils/token"; -import { nanoid } from "nanoid"; import { createPersistStore } from "../utils/store"; -import { collectModelsWithDefaultModel } from "../utils/model"; -import { useAccessStore } from "./access"; -import { isDalle3, safeLocalStorage } from "../utils"; -import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; +import { estimateTokenLength } from "../utils/token"; +import { ModelConfig, ModelType, useAppConfig } from "./config"; +import { createEmptyMask, Mask } from "./mask"; const localStorage = safeLocalStorage(); @@ -114,39 +117,6 @@ function createEmptySession(): ChatSession { // if it is using gpt-* models, force to use 4o-mini to summarize const ChatFetchTaskPool: Record = {}; -function getSummarizeModel(currentModel: string): { - name: string; - providerName: string | undefined; -} { - // if it is using gpt-* models, force to use 4o-mini to summarize - if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) { - const configStore = useAppConfig.getState(); - const accessStore = useAccessStore.getState(); - const allModel = collectModelsWithDefaultModel( - configStore.models, - [configStore.customModels, accessStore.customModels].join(","), - accessStore.defaultModel, - ); - const summarizeModel = allModel.find( - (m) => m.name === SUMMARIZE_MODEL && m.available, - ); - return { - name: summarizeModel?.name ?? currentModel, - providerName: summarizeModel?.provider?.providerName, - }; - } - if (currentModel.startsWith("gemini")) { - return { - name: GEMINI_SUMMARIZE_MODEL, - providerName: ServiceProvider.Google, - }; - } - return { - name: currentModel, - providerName: undefined, - }; -} - function countMessages(msgs: ChatMessage[]) { return msgs.reduce( (pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)), @@ -935,7 +905,7 @@ export const useChatStore = createPersistStore( return; } - const providerName = modelConfig.providerName; + const providerName = modelConfig.compressProviderName; const api: ClientApi = getClientApi(providerName); // remove error messages if any @@ -957,9 +927,7 @@ export const useChatStore = createPersistStore( api.llm.chat({ messages: topicMessages, config: { - model: getSummarizeModel(session.mask.modelConfig.model).name, - providerName: getSummarizeModel(session.mask.modelConfig.model) - .providerName, + model: modelConfig.compressModel, stream: false, }, onFinish(message) { @@ -1021,9 +989,10 @@ export const useChatStore = createPersistStore( config: { ...modelcfg, stream: true, - model: getSummarizeModel(session.mask.modelConfig.model).name, - providerName: getSummarizeModel(session.mask.modelConfig.model) - .providerName, + model: modelConfig.compressModel, + // providerName: getSummarizeModel(session.mask.modelConfig.model) + // .providerName, + // TODO: }, onUpdate(message) { session.memoryPrompt = message; @@ -1072,7 +1041,7 @@ export const useChatStore = createPersistStore( }, { name: StoreKey.Chat, - version: 3.1, + version: 3.2, migrate(persistedState, version) { const state = persistedState as any; const newState = JSON.parse( @@ -1119,6 +1088,16 @@ export const useChatStore = createPersistStore( }); } + // add default summarize model for every session + if (version < 3.2) { + newState.sessions.forEach((s) => { + const config = useAppConfig.getState(); + s.mask.modelConfig.compressModel = config.modelConfig.compressModel; + s.mask.modelConfig.compressProviderName = + config.modelConfig.compressProviderName; + }); + } + return newState as any; }, }, diff --git a/app/store/config.ts b/app/store/config.ts index e7b40a914..3e59c806a 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -55,7 +55,7 @@ export const DEFAULT_CONFIG = { dontUseModel: DISABLE_MODELS, modelConfig: { - model: "gpt-3.5-turbo-0125" as ModelType, + model: "gpt-4o-mini" as ModelType, providerName: "OpenAI" as ServiceProvider, temperature: 0.8, top_p: 1, @@ -65,6 +65,8 @@ export const DEFAULT_CONFIG = { sendMemory: true, historyMessageCount: 5, compressMessageLengthThreshold: 4000, + compressModel: "gpt-4o-mini" as ModelType, + compressProviderName: "OpenAI" as ServiceProvider, enableInjectSystemPrompts: true, template: config?.template ?? DEFAULT_INPUT_TEMPLATE, size: "1024x1024" as DalleSize, @@ -145,7 +147,7 @@ export const useAppConfig = createPersistStore( }), { name: StoreKey.Config, - version: 3.993, + version: 4, migrate(persistedState, version) { const state = persistedState as ChatConfig; @@ -190,6 +192,13 @@ export const useAppConfig = createPersistStore( // : config?.template ?? DEFAULT_INPUT_TEMPLATE; } + if (version < 4) { + state.modelConfig.compressModel = + DEFAULT_CONFIG.modelConfig.compressModel; + state.modelConfig.compressProviderName = + DEFAULT_CONFIG.modelConfig.compressProviderName; + } + return state as any; }, }, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 78835d24d..2a19c9332 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.15.1" + "version": "2.15.2" }, "tauri": { "allowlist": {