feat: add support for displaying thinking/reasoning blocks in chat
This commit is contained in:
committed by
Nathan Esquenazi
parent
907bb224d9
commit
8ff5d83e14
@@ -84,6 +84,11 @@ async function send(){
|
|||||||
let assistantText='';
|
let assistantText='';
|
||||||
let assistantRow=null;
|
let assistantRow=null;
|
||||||
let assistantBody=null;
|
let assistantBody=null;
|
||||||
|
// Thinking tag patterns for streaming display
|
||||||
|
const _thinkPairs=[
|
||||||
|
{open:'<think>',close:'</think>'},
|
||||||
|
{open:'<|channel>thought\n',close:'<channel|>'}
|
||||||
|
];
|
||||||
|
|
||||||
function ensureAssistantRow(){
|
function ensureAssistantRow(){
|
||||||
if(assistantRow)return;
|
if(assistantRow)return;
|
||||||
@@ -106,12 +111,32 @@ async function send(){
|
|||||||
|
|
||||||
// rAF-throttled rendering: buffer tokens, render at most once per frame
|
// rAF-throttled rendering: buffer tokens, render at most once per frame
|
||||||
let _renderPending=false;
|
let _renderPending=false;
|
||||||
|
// Extract display text from assistantText, stripping completed thinking blocks
|
||||||
|
// and hiding content still inside an open thinking block.
|
||||||
|
function _streamDisplay(){
|
||||||
|
let t=assistantText;
|
||||||
|
for(const {open,close} of _thinkPairs){
|
||||||
|
if(!t.startsWith(open)) continue;
|
||||||
|
const ci=t.indexOf(close,open.length);
|
||||||
|
if(ci!==-1){
|
||||||
|
// Thinking block complete — strip it, show the rest
|
||||||
|
return t.slice(ci+close.length).replace(/^\s+/,'');
|
||||||
|
}
|
||||||
|
// Still inside thinking block — show placeholder
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
function _scheduleRender(){
|
function _scheduleRender(){
|
||||||
if(_renderPending) return;
|
if(_renderPending) return;
|
||||||
_renderPending=true;
|
_renderPending=true;
|
||||||
requestAnimationFrame(()=>{
|
requestAnimationFrame(()=>{
|
||||||
_renderPending=false;
|
_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?'<span style="color:var(--muted);font-size:13px">Thinking\u2026</span>':'');
|
||||||
|
}
|
||||||
scrollIfPinned();
|
scrollIfPinned();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
16
static/ui.js
16
static/ui.js
@@ -503,6 +503,22 @@ function renderMessages(){
|
|||||||
if(!thinkingText && m.reasoning){
|
if(!thinkingText && m.reasoning){
|
||||||
thinkingText=m.reasoning;
|
thinkingText=m.reasoning;
|
||||||
}
|
}
|
||||||
|
// Parse inline thinking tags from plain text: <think>...</think> (DeepSeek, QwQ, etc.)
|
||||||
|
// and Gemma 4 channel tokens: <|channel>thought\n...<channel|>
|
||||||
|
if(!thinkingText && typeof content==='string'){
|
||||||
|
const thinkMatch=content.match(/^<think>([\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]*?)<channel\|>\s*/);
|
||||||
|
if(gemmaMatch){
|
||||||
|
thinkingText=gemmaMatch[1].trim();
|
||||||
|
content=content.slice(gemmaMatch[0].length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const isUser=m.role==='user';
|
const isUser=m.role==='user';
|
||||||
const isLastAssistant=!isUser&&vi===visWithIdx.length-1;
|
const isLastAssistant=!isUser&&vi===visWithIdx.length-1;
|
||||||
// Render thinking card before the assistant message (collapsed by default)
|
// Render thinking card before the assistant message (collapsed by default)
|
||||||
|
|||||||
Reference in New Issue
Block a user