Append fade spans instead of replaying streamed prose
This commit is contained in:
1 parent
fbebd0bd30
commit
af2f534f21
2 files changed
+121
-12
No files matched your search
+54
-9
@@ -2547,6 +2547,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
let _streamFadeVisibleWords=0;
|
||||
let _streamFadeHoldUntilMs=0;
|
||||
let _streamFadeCurrentMs=200;
|
||||
let _streamFadeDomText='';
|
||||
let _streamFadeReduceMotionMql=null;
|
||||
let _streamFadeReduceMotion=false;
|
||||
let _streamFadeReduceMotionOnChange=null;
|
||||
@@ -4091,6 +4092,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
_streamFadeVisibleWords=0;
|
||||
_streamFadeHoldUntilMs=0;
|
||||
_streamFadeCurrentMs=_STREAM_FADE_MS;
|
||||
_streamFadeDomText='';
|
||||
}
|
||||
function _cancelAnimationFramePendingStreamRender(){
|
||||
if(_pendingRafHandle===null) return;
|
||||
@@ -4230,6 +4232,43 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const m=String(text||'').match(/\S+/g);
|
||||
return m?m.length:0;
|
||||
}
|
||||
function _streamFadeAppendText(el, text){
|
||||
if(!el) return;
|
||||
const value=String(text||'');
|
||||
if(!value) return;
|
||||
const reduceMotion=_streamFadeReduceMotionEnabled();
|
||||
const frag=document.createDocumentFragment();
|
||||
const wordRe=/(\S+)(\s*)/g;
|
||||
const appendStartedAt=performance.now();
|
||||
let last=0, match, changed=false;
|
||||
_streamFadeAppendOffset=0;
|
||||
while((match=wordRe.exec(value))){
|
||||
if(match.index>last) frag.appendChild(document.createTextNode(value.slice(last,match.index)));
|
||||
if(reduceMotion){
|
||||
frag.appendChild(document.createTextNode(match[1]));
|
||||
}else{
|
||||
const span=document.createElement('span');
|
||||
span.className='stream-fade-word is-new';
|
||||
const fadeMs=_streamFadeCurrentMs||_STREAM_FADE_MS;
|
||||
const delayMs=_streamFadeAppendOffset*_STREAM_FADE_STAGGER_MS;
|
||||
span.style.animationDelay=delayMs+'ms';
|
||||
if(fadeMs!==_STREAM_FADE_MS) span.style.setProperty('--stream-fade-ms',fadeMs+'ms');
|
||||
span.textContent=match[1];
|
||||
frag.appendChild(span);
|
||||
_streamFadeAppendOffset+=1;
|
||||
_streamFadeLatestAnimationEndAt=Math.max(_streamFadeLatestAnimationEndAt,appendStartedAt+delayMs+fadeMs);
|
||||
}
|
||||
if(match[2]) frag.appendChild(document.createTextNode(match[2]));
|
||||
last=match.index+match[0].length;
|
||||
changed=true;
|
||||
}
|
||||
if(!changed){
|
||||
frag.appendChild(document.createTextNode(value));
|
||||
}else if(last<value.length){
|
||||
frag.appendChild(document.createTextNode(value.slice(last)));
|
||||
}
|
||||
el.appendChild(frag);
|
||||
}
|
||||
function _streamFadePauseAfter(text, paragraphBreakIndex){
|
||||
if(paragraphBreakIndex>=0) return 90;
|
||||
const trimmed=String(text||'').trimEnd();
|
||||
@@ -4333,17 +4372,23 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const next=_streamFadeNextText(displayText);
|
||||
if(!next.changed) return next.caughtUp;
|
||||
assistantBody.classList.add('stream-fade-active');
|
||||
if(!_smdParser&&window.smd){
|
||||
if(_smdReconnect){assistantBody.innerHTML='';_smdReconnect=false;}
|
||||
_smdNewParser(assistantBody,true);
|
||||
}
|
||||
_streamFadeBindCleanup(assistantBody);
|
||||
if(_smdParser){
|
||||
_streamFadeAppendOffset=0;
|
||||
_smdWrite(next.text,true);
|
||||
}else{
|
||||
assistantBody.innerHTML=renderMd ? renderMd(next.text||'') : esc(next.text||'');
|
||||
_sanitizeSmdLinks(assistantBody);
|
||||
_smdEndParser();
|
||||
assistantBody.textContent='';
|
||||
_streamFadeDomText='';
|
||||
}
|
||||
_smdReconnect=false;
|
||||
if(!_streamFadeDomText&&assistantBody.textContent){
|
||||
assistantBody.textContent='';
|
||||
}
|
||||
if(!String(next.text||'').startsWith(_streamFadeDomText)){
|
||||
assistantBody.textContent='';
|
||||
_streamFadeDomText='';
|
||||
}
|
||||
const delta=String(next.text||'').slice(_streamFadeDomText.length);
|
||||
_streamFadeAppendText(assistantBody,delta);
|
||||
_streamFadeDomText=String(next.text||'');
|
||||
return next.caughtUp;
|
||||
}
|
||||
function _streamFadeCurrentDisplayText(){
|
||||
|
||||
@@ -86,6 +86,7 @@ let _streamFadeAppendOffset=0;
|
||||
let _streamFadeVisibleWords=0;
|
||||
let _streamFadeHoldUntilMs=0;
|
||||
let _streamFadeCurrentMs=200;
|
||||
let _streamFadeDomText='';
|
||||
const _STREAM_FADE_MS=200;
|
||||
const _STREAM_FADE_MAX_MS=350;
|
||||
const _STREAM_FADE_STAGGER_MS=16;
|
||||
@@ -166,12 +167,24 @@ def test_stream_fade_uses_incremental_renderer_without_changing_default_path():
|
||||
[
|
||||
"_streamFadeNextText(displayText)",
|
||||
"if(!next.changed) return next.caughtUp",
|
||||
"_smdNewParser(assistantBody,true)",
|
||||
"_smdWrite(next.text,true)",
|
||||
"_streamFadeBindCleanup(assistantBody)",
|
||||
"_streamFadeAppendText(assistantBody,delta)",
|
||||
"_streamFadeDomText=String(next.text||'')",
|
||||
"stream-fade-active",
|
||||
],
|
||||
)
|
||||
assert "renderMd ? renderMd(next.text||'')" in render_block
|
||||
assert "_smdWrite(next.text,true)" not in render_block
|
||||
assert "innerHTML" not in render_block
|
||||
append_block = function_block(MESSAGES_JS, "_streamFadeAppendText")
|
||||
assert_contains_all(
|
||||
append_block,
|
||||
[
|
||||
"document.createDocumentFragment()",
|
||||
"span.className='stream-fade-word is-new'",
|
||||
"el.appendChild(frag)",
|
||||
"_streamFadeLatestAnimationEndAt",
|
||||
],
|
||||
)
|
||||
assert_contains_all(
|
||||
renderer_block,
|
||||
[
|
||||
@@ -191,6 +204,57 @@ def test_stream_fade_uses_incremental_renderer_without_changing_default_path():
|
||||
assert "_wrapStreamingFadeWords" not in MESSAGES_JS
|
||||
|
||||
|
||||
def test_stream_fade_appends_new_spans_without_replacing_existing_nodes():
|
||||
script = (
|
||||
function_block(MESSAGES_JS, "_streamFadeAppendText")
|
||||
+ r"""
|
||||
const _STREAM_FADE_MS=200;
|
||||
const _STREAM_FADE_STAGGER_MS=16;
|
||||
let _streamFadeAppendOffset=0;
|
||||
let _streamFadeLatestAnimationEndAt=0;
|
||||
let _streamFadeCurrentMs=200;
|
||||
const performance={_t:0,now(){return this._t;}};
|
||||
function _streamFadeReduceMotionEnabled(){ return false; }
|
||||
class FakeNode{
|
||||
constructor(type,text=''){
|
||||
this.type=type;
|
||||
this.children=[];
|
||||
this.className='';
|
||||
this.textContent=text;
|
||||
this.style={values:{},setProperty:(name,value)=>{this.style.values[name]=value;}};
|
||||
}
|
||||
appendChild(child){
|
||||
if(child&&child.type==='fragment'){
|
||||
child.children.forEach(n=>this.children.push(n));
|
||||
}else{
|
||||
this.children.push(child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
global.document={
|
||||
createDocumentFragment(){ return new FakeNode('fragment'); },
|
||||
createTextNode(text){ return new FakeNode('text',String(text)); },
|
||||
createElement(tag){ const node=new FakeNode(tag); node.tagName=String(tag).toUpperCase(); return node; },
|
||||
};
|
||||
const body=new FakeNode('div');
|
||||
_streamFadeAppendText(body,'alpha beta ');
|
||||
const firstSpan=body.children.find(node=>node.className==='stream-fade-word is-new');
|
||||
if(!firstSpan) throw new Error('missing first fade span');
|
||||
_streamFadeAppendText(body,'gamma');
|
||||
if(body.children.find(node=>node.className==='stream-fade-word is-new')!==firstSpan){
|
||||
throw new Error('first span was replaced');
|
||||
}
|
||||
const spans=body.children.filter(node=>node.className==='stream-fade-word is-new');
|
||||
if(spans.length!==3) throw new Error(`expected three animated spans, got ${spans.length}`);
|
||||
if(spans.map(node=>node.textContent).join('|')!=='alpha|beta|gamma'){
|
||||
throw new Error(`wrong span text: ${spans.map(node=>node.textContent).join('|')}`);
|
||||
}
|
||||
"""
|
||||
)
|
||||
run_node(script)
|
||||
|
||||
|
||||
def test_transparent_anchor_prose_uses_fade_renderer_when_enabled():
|
||||
anchor_block = function_block(MESSAGES_JS, "_anchorProseIncrementalNode")
|
||||
assert_contains_all(
|
||||
|
||||
Reference in new issue
Block a user