diff --git a/static/messages.js b/static/messages.js index 284ca5b..0209c2f 100644 --- a/static/messages.js +++ b/static/messages.js @@ -84,6 +84,11 @@ async function send(){ let assistantText=''; let assistantRow=null; let assistantBody=null; + // Thinking tag patterns for streaming display + const _thinkPairs=[ + {open:'',close:''}, + {open:'<|channel>thought\n',close:''} + ]; function ensureAssistantRow(){ if(assistantRow)return; @@ -106,12 +111,36 @@ async function send(){ // rAF-throttled rendering: buffer tokens, render at most once per frame let _renderPending=false; + // Extract display text from assistantText, stripping completed thinking blocks + // and hiding content still inside an open thinking block. + function _streamDisplay(){ + const raw=assistantText; + for(const {open,close} of _thinkPairs){ + if(raw.startsWith(open)){ + const ci=raw.indexOf(close,open.length); + if(ci!==-1){ + // Thinking block complete — strip it, show the rest + return raw.slice(ci+close.length).replace(/^\s+/,''); + } + // Still inside thinking block — show placeholder + return ''; + } + // Hide partial tag prefixes while streaming so users don't see + // `{ _renderPending=false; - if(assistantBody) assistantBody.innerHTML=renderMd(assistantText); + if(assistantBody){ + const txt=_streamDisplay(); + const isThinking=!txt&&assistantText.length>0; + assistantBody.innerHTML=txt?renderMd(txt):(isThinking?'Thinking\u2026':''); + } scrollIfPinned(); }); } diff --git a/static/ui.js b/static/ui.js index f318a32..3b5ab78 100644 --- a/static/ui.js +++ b/static/ui.js @@ -503,6 +503,22 @@ function renderMessages(){ if(!thinkingText && m.reasoning){ thinkingText=m.reasoning; } + // Parse inline thinking tags from plain text: ... (DeepSeek, QwQ, etc.) + // and Gemma 4 channel tokens: <|channel>thought\n... + if(!thinkingText && typeof content==='string'){ + const thinkMatch=content.match(/^([\s\S]*?)<\/think>\s*/); + if(thinkMatch){ + thinkingText=thinkMatch[1].trim(); + content=content.slice(thinkMatch[0].length); + } + if(!thinkingText){ + const gemmaMatch=content.match(/^<\|channel>thought\n([\s\S]*?)\s*/); + if(gemmaMatch){ + thinkingText=gemmaMatch[1].trim(); + content=content.slice(gemmaMatch[0].length); + } + } + } const isUser=m.role==='user'; const isLastAssistant=!isUser&&vi===visWithIdx.length-1; // Render thinking card before the assistant message (collapsed by default)