diff --git a/app/client/api.ts b/app/client/api.ts index b04cf9b8..402bac43 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -33,6 +33,7 @@ export interface LLMConfig { stream?: boolean; presence_penalty?: number; frequency_penalty?: number; + enable_thinking?: boolean; } export interface ChatOptions { diff --git a/app/client/webllm.ts b/app/client/webllm.ts index e177ca81..9e739db1 100644 --- a/app/client/webllm.ts +++ b/app/client/webllm.ts @@ -84,6 +84,20 @@ export class WebLLMApi implements LLMApi { async chat(options: ChatOptions): Promise { if (!this.initialized || this.isDifferentConfig(options.config)) { this.llmConfig = { ...(this.llmConfig || {}), ...options.config }; + // Check if this is a Qwen3 model with thinking mode enabled + const isQwen3Model = this.llmConfig?.model + ?.toLowerCase() + .startsWith("qwen3"); + const isThinkingEnabled = this.llmConfig?.enable_thinking === true; + + // Apply special config for Qwen3 models with thinking mode enabled + if (isQwen3Model && isThinkingEnabled && this.llmConfig) { + this.llmConfig = { + ...this.llmConfig, + temperature: 0.6, + top_p: 0.95, + }; + } try { await this.initModel(options.onUpdate); } catch (err: any) { @@ -160,13 +174,14 @@ export class WebLLMApi implements LLMApi { "stream", "presence_penalty", "frequency_penalty", + "enable_thinking", ]; for (const field of optionalFields) { if ( this.llmConfig[field] !== undefined && config[field] !== undefined && - config[field] !== config[field] + this.llmConfig[field] !== config[field] ) { return true; } @@ -184,10 +199,39 @@ export class WebLLMApi implements LLMApi { usage?: CompletionUsage, ) => void, ) { + // For Qwen3 models, we need to filter out the ... content + // Do not do it inplace, create a new messages array + let newMessages: RequestMessage[] | undefined; + const isQwen3Model = this.llmConfig?.model + ?.toLowerCase() + .startsWith("qwen3"); + if (isQwen3Model) { + newMessages = messages.map((message) => { + const newMessage = { ...message }; + if ( + message.role === "assistant" && + typeof message.content === "string" + ) { + newMessage.content = message.content.replace( + /^[\s\S]*?<\/think>\n?\n?/, + "", + ); + } + return newMessage; + }); + } + + // Prepare extra_body with enable_thinking option for Qwen3 models + const extraBody: Record = {}; + if (isQwen3Model) { + extraBody.enable_thinking = this.llmConfig?.enable_thinking ?? false; + } + const completion = await this.webllm.engine.chatCompletion({ stream: stream, - messages: messages as ChatCompletionMessageParam[], + messages: (newMessages || messages) as ChatCompletionMessageParam[], ...(stream ? { stream_options: { include_usage: true } } : {}), + ...(Object.keys(extraBody).length > 0 ? { extra_body: extraBody } : {}), }); if (stream) { diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index d9bcc00b..bd462d10 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -71,6 +71,10 @@ width: var(--icon-width); overflow: hidden; + &.selected { + background-color: var(--second); + } + &:not(:last-child) { margin-right: 5px; } diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 830564b2..a363a18a 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -27,6 +27,7 @@ import DeleteIcon from "../icons/clear.svg"; import EditIcon from "../icons/rename.svg"; import ConfirmIcon from "../icons/confirm.svg"; import ImageIcon from "../icons/image.svg"; +import BrainIcon from "../icons/brain.svg"; import BottomIcon from "../icons/bottom.svg"; import StopIcon from "../icons/pause.svg"; @@ -385,6 +386,7 @@ function ChatAction(props: { icon: JSX.Element; onClick: () => void; fullWidth?: boolean; + selected?: boolean; }) { const iconRef = useRef(null); const textRef = useRef(null); @@ -406,7 +408,7 @@ function ChatAction(props: { return props.fullWidth ? (
@@ -418,7 +420,7 @@ function ChatAction(props: {
) : (
{ props.onClick(); setTimeout(updateWidth, 1); @@ -535,6 +537,18 @@ export function ChatActions(props: { }); }} /> + {config.modelConfig.model.toLowerCase().startsWith("qwen3") && ( + + config.update( + (config) => (config.enableThinking = !config.enableThinking), + ) + } + text={Locale.Settings.THINKING} + icon={} + selected={config.enableThinking} + /> + )} setShowModelSelector(true)} text={currentModel} diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx index 2ea8a068..c12154e5 100644 --- a/app/components/model-config.tsx +++ b/app/components/model-config.tsx @@ -83,6 +83,24 @@ export function ModelConfigList() { + {config.modelConfig.model.toLowerCase().startsWith("qwen3") && ( + + + config.update( + (config) => + (config.enableThinking = e.currentTarget.checked), + ) + } + > + + )} + {/* New setting item for LLM model context window length */} \s*<\/think>/g, ""); + } botMessage.content = message; get().onNewMessage(botMessage, llm); } @@ -532,6 +536,7 @@ export const useChatStore = createPersistStore( model: modelConfig.model, cache: useAppConfig.getState().cacheType, stream: false, + enable_thinking: false, // never think for topic }, onFinish(message) { get().updateCurrentSession( @@ -615,6 +620,7 @@ export const useChatStore = createPersistStore( stream: true, model: modelConfig.model, cache: useAppConfig.getState().cacheType, + enable_thinking: false, // never think for summarization }, onUpdate(message) { session.memoryPrompt = message; diff --git a/app/store/config.ts b/app/store/config.ts index fa8c7e4f..416734bb 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -75,6 +75,7 @@ export type ConfigType = { cacheType: CacheType; logLevel: LogLevel; + enableThinking: boolean; modelConfig: ModelConfig; }; @@ -124,6 +125,7 @@ export const DEFAULT_CONFIG: ConfigType = { models: DEFAULT_MODELS, cacheType: CacheType.Cache, logLevel: "INFO", + enableThinking: false, modelConfig: DEFAULT_MODEL_CONFIG, }; @@ -217,9 +219,9 @@ export const useAppConfig = createPersistStore( }), { name: StoreKey.Config, - version: 0.62, + version: 0.64, migrate: (persistedState, version) => { - if (version < 0.62) { + if (version < 0.64) { return { ...DEFAULT_CONFIG, ...(persistedState as any), diff --git a/app/utils.ts b/app/utils.ts index 4d184369..35ea6ffd 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -10,11 +10,14 @@ export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language // This will remove the specified punctuation from the end of the string // and also trim quotes from both the start and end if they exist. + console.log("TrimTopic", topic); return ( topic // fix for gemini - .replace(/^["“”*]+|["“”*]+$/g, "") - .replace(/[,。!?”“"、,.!?*]*$/, "") + .replace(/^["""*]+|["""*]+$/g, "") + .replace(/[,。!?"""、,.!?*]*$/, "") + // remove think tags and content between them, including across multiple lines + .replace(/[\s\S]*?<\/think>/g, "") ); } diff --git a/package.json b/package.json index 4f23b555..699fdfbd 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "@fortaine/fetch-event-source": "^3.0.6", "@hello-pangea/dnd": "^16.5.0", - "@mlc-ai/web-llm": "^0.2.78", + "@mlc-ai/web-llm": "^0.2.79", "@serwist/next": "^9.0.2", "@svgr/webpack": "^6.5.1", "emoji-picker-react": "^4.9.2", diff --git a/yarn.lock b/yarn.lock index c5a6934a..20ac0479 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1180,10 +1180,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@mlc-ai/web-llm@^0.2.78": - version "0.2.78" - resolved "https://registry.yarnpkg.com/@mlc-ai/web-llm/-/web-llm-0.2.78.tgz#f9ce70319b86bb8c0dd4b1a0476152e4fd3e82be" - integrity sha512-ptqDNzHnfDyNZj7vjp9IaY5U/QDweXMe5wNzErOmRT1gqj8AaMvcqbj7HroPDzhXJGM7BZpDjANV5MhXhKOosA== +"@mlc-ai/web-llm@^0.2.79": + version "0.2.79" + resolved "https://registry.yarnpkg.com/@mlc-ai/web-llm/-/web-llm-0.2.79.tgz#a0dcfc54bf5d843090be67fd9b168e4de087bc93" + integrity sha512-Hy1ZHQ0o2bZGZoVnGK48+fts/ZSKwLe96xjvqL/6C59Mem9HoHTcFE07NC2E23mRmhd01tL655N6CPeYmwWgwQ== dependencies: loglevel "^1.9.1"