commit
e6b2ac9119
6 files changed
+488
-56
No files matched your search
+92
-29
@@ -2546,10 +2546,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
let _streamFadeLastArrivalMs=0;
|
||||
let _streamFadeArrivalWps=0;
|
||||
let _streamFadeLatestAnimationEndAt=0;
|
||||
let _streamFadeAppendOffset=0;
|
||||
let _streamFadeVisibleWords=0;
|
||||
let _streamFadeHoldUntilMs=0;
|
||||
let _streamFadeCurrentMs=200;
|
||||
let _streamFadeCurrentMs=620;
|
||||
let _streamFadeDomText='';
|
||||
let _streamFadeReduceMotionMql=null;
|
||||
let _streamFadeReduceMotion=false;
|
||||
let _streamFadeReduceMotionOnChange=null;
|
||||
@@ -2560,11 +2560,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
? Number((INFLIGHT[activeSid]&&INFLIGHT[activeSid].lastRunJournalSeq)||0)
|
||||
: 0;
|
||||
let _lastRunJournalEventId='';
|
||||
const _STREAM_FADE_MS=200;
|
||||
const _STREAM_FADE_MAX_MS=350;
|
||||
const _STREAM_FADE_STAGGER_MS=16;
|
||||
const _STREAM_FADE_DONE_MAX_MS=320;
|
||||
const _STREAM_FADE_DONE_DRAIN_MAX_MS=900;
|
||||
const _STREAM_FADE_MS=620;
|
||||
const _STREAM_FADE_MAX_MS=900;
|
||||
const _STREAM_FADE_DONE_MAX_MS=1000;
|
||||
const _STREAM_FADE_DONE_DRAIN_MAX_MS=1400;
|
||||
const _anchorApi=(typeof window!=='undefined'&&window.HermesAssistantTurnAnchors)
|
||||
? window.HermesAssistantTurnAnchors
|
||||
: null;
|
||||
@@ -3669,21 +3668,24 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
function _anchorProseIncrementalNode(key, text){
|
||||
if(!window.smd || !key || typeof _safeSmdRenderer!=='function') return null;
|
||||
const value=String(text||'');
|
||||
const fade=typeof _shouldUseLiveProseFade==='function'&&_shouldUseLiveProseFade();
|
||||
try{
|
||||
let st=_anchorProseSmdCache.get(key);
|
||||
// Self-heal desyncs (edit/sanitize made the text no longer a pure append):
|
||||
// rebuild the parser+node from scratch, mirroring _smdWrite's guard.
|
||||
if(st && st.writtenText && !value.startsWith(st.writtenText)) st=null;
|
||||
if(st && st.fade!==fade) st=null;
|
||||
if(!st){
|
||||
const node=document.createElement('div');
|
||||
node.className='assistant-segment';
|
||||
node.setAttribute('data-anchor-scene-prose','1');
|
||||
const body=document.createElement('div');
|
||||
body.className='msg-body';
|
||||
if(body.classList) body.classList.toggle('stream-fade-active',fade);
|
||||
node.appendChild(body);
|
||||
const baseRenderer=_safeSmdRenderer(body);
|
||||
const baseRenderer=fade?_streamFadeRenderer(body):_safeSmdRenderer(body);
|
||||
const renderer=_smdRendererWithoutUnderscoreEmphasis(baseRenderer);
|
||||
st={node,parser:window.smd.parser(renderer),writtenText:''};
|
||||
st={node,parser:window.smd.parser(renderer),writtenText:'',fade};
|
||||
_anchorProseSmdCache.set(key,st);
|
||||
// Bound memory across turns: keys embed the stream id, so stale entries
|
||||
// from finished streams age out here.
|
||||
@@ -3692,6 +3694,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(oldest!==key) _anchorProseSmdCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
const body=st.node&&st.node.querySelector&&st.node.querySelector('.msg-body');
|
||||
if(body&&body.classList) body.classList.toggle('stream-fade-active',fade);
|
||||
const delta=value.slice(st.writtenText.length);
|
||||
if(delta){
|
||||
window.smd.parser_write(st.parser,delta);
|
||||
@@ -4114,10 +4118,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
_streamFadeLastArrivalMs=0;
|
||||
_streamFadeArrivalWps=0;
|
||||
_streamFadeLatestAnimationEndAt=0;
|
||||
_streamFadeAppendOffset=0;
|
||||
_streamFadeVisibleWords=0;
|
||||
_streamFadeHoldUntilMs=0;
|
||||
_streamFadeCurrentMs=_STREAM_FADE_MS;
|
||||
_streamFadeDomText='';
|
||||
}
|
||||
function _cancelAnimationFramePendingStreamRender(){
|
||||
if(_pendingRafHandle===null) return;
|
||||
@@ -4129,6 +4133,12 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
function _shouldUseStreamFade(){
|
||||
return window._fadeTextEffect===true;
|
||||
}
|
||||
function _shouldUseTransparentStreamFade(){
|
||||
return typeof isTransparentStream==='function'&&isTransparentStream();
|
||||
}
|
||||
function _shouldUseLiveProseFade(){
|
||||
return !_streamFadeReduceMotionEnabled() && (_shouldUseStreamFade() || _shouldUseTransparentStreamFade());
|
||||
}
|
||||
function _streamFadeSkipNode(node){
|
||||
if(!node||node.nodeType!==1) return false;
|
||||
const tag=(node.tagName||'').toLowerCase();
|
||||
@@ -4187,13 +4197,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
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);
|
||||
_streamFadeLatestAnimationEndAt=Math.max(_streamFadeLatestAnimationEndAt,appendStartedAt+fadeMs);
|
||||
if(match[2]) frag.appendChild(document.createTextNode(match[2]));
|
||||
last=match.index+match[0].length;
|
||||
changed=true;
|
||||
@@ -4257,6 +4264,39 @@ 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;
|
||||
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;
|
||||
if(fadeMs!==_STREAM_FADE_MS) span.style.setProperty('--stream-fade-ms',fadeMs+'ms');
|
||||
span.textContent=match[1];
|
||||
frag.appendChild(span);
|
||||
_streamFadeLatestAnimationEndAt=Math.max(_streamFadeLatestAnimationEndAt,appendStartedAt+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();
|
||||
@@ -4360,17 +4400,36 @@ 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);
|
||||
if(!_shouldUseTransparentStreamFade()){
|
||||
if(!_smdParser&&window.smd){
|
||||
if(_smdReconnect){assistantBody.innerHTML='';_smdReconnect=false;}
|
||||
_smdNewParser(assistantBody,true);
|
||||
}
|
||||
if(_smdParser){
|
||||
_smdWrite(next.text,true);
|
||||
}else{
|
||||
assistantBody.innerHTML=renderMd ? renderMd(next.text||'') : esc(next.text||'');
|
||||
_sanitizeSmdLinks(assistantBody);
|
||||
}
|
||||
_streamFadeDomText=String(next.text||'');
|
||||
return next.caughtUp;
|
||||
}
|
||||
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);
|
||||
if(delta) assistantBody.appendChild(document.createTextNode(delta));
|
||||
_streamFadeDomText=String(next.text||'');
|
||||
return next.caughtUp;
|
||||
}
|
||||
function _streamFadeCurrentDisplayText(){
|
||||
@@ -4386,6 +4445,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(!assistantBody){onDone();return;}
|
||||
const target=_streamFadeCurrentDisplayText();
|
||||
const caughtUp=_renderStreamingFadeMarkdown(target);
|
||||
const anchorProcessText=_streamFadeDomText||target;
|
||||
if(anchorProcessText) _upsertAnchorProcessProse(anchorProcessText);
|
||||
scrollIfPinned();
|
||||
if(caughtUp){
|
||||
// parser_end can flush pending markdown text; include that final text in
|
||||
@@ -4760,12 +4821,14 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const parsed=_cachedParsed&&_cachedParsedText===assistantText&&_cachedParsedReasoning===liveReasoningText ? _cachedParsed : _parseStreamState();
|
||||
_cachedParsed=null;
|
||||
_renderLiveThinking(parsed);
|
||||
const displayText = segmentStart===0
|
||||
? parsed.displayText // first segment: uses think-tag stripping
|
||||
: _stripXmlToolCalls(assistantText.slice(segmentStart));
|
||||
let anchorProcessText=displayText;
|
||||
if(assistantBody){
|
||||
const displayText = segmentStart===0
|
||||
? parsed.displayText // first segment: uses think-tag stripping
|
||||
: _stripXmlToolCalls(assistantText.slice(segmentStart));
|
||||
if(_shouldUseStreamFade()){
|
||||
if(_shouldUseLiveProseFade()){
|
||||
const caughtUp=_renderStreamingFadeMarkdown(displayText);
|
||||
anchorProcessText=_streamFadeDomText||'';
|
||||
if(!caughtUp&&!_streamFinalized){
|
||||
setTimeout(()=>_scheduleRender(), 33);
|
||||
}
|
||||
@@ -4790,13 +4853,13 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
assistantBody.innerHTML = renderMd ? renderMd(fallbackText) : esc(fallbackText);
|
||||
}
|
||||
}
|
||||
_upsertAnchorProcessProse(displayText);
|
||||
if(typeof _syncLiveWorklogReasonsForAnchor==='function') _syncLiveWorklogReasonsForAnchor(assistantRow, displayText);
|
||||
}
|
||||
if(anchorProcessText) _upsertAnchorProcessProse(anchorProcessText);
|
||||
scrollIfPinned();
|
||||
_throttledSnapshotLiveTurn();
|
||||
};
|
||||
const frameIntervalMs=_shouldUseStreamFade()?33:66;
|
||||
const frameIntervalMs=_shouldUseLiveProseFade()?33:66;
|
||||
if(sinceLastMs>=frameIntervalMs){
|
||||
_pendingRafHandle=requestAnimationFrame(_doRender);
|
||||
} else {
|
||||
@@ -5489,7 +5552,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
});
|
||||
sendBrowserNotification('Response complete',_completionPreview||'Task finished',{forceHidden:_wasEverBackgrounded,sid:activeSid});
|
||||
};
|
||||
if(_shouldUseStreamFade()&&assistantBody){
|
||||
if(_shouldUseLiveProseFade()&&assistantBody){
|
||||
_cancelAnimationFramePendingStreamRender();
|
||||
_drainStreamFadeBeforeDone(_finishDone);
|
||||
return;
|
||||
|
||||
+2
-2
@@ -6905,8 +6905,8 @@ main.main.showing-logs > #mainLogs{display:flex;}
|
||||
/* OpenWebUI-style streaming word fade (opt-in via Settings → Preferences).
|
||||
Opacity-only fade with JS-paced word/paragraph reveal. */
|
||||
.stream-fade-active .stream-fade-word{display:inline;}
|
||||
.stream-fade-word.is-new{animation:stream-fade-word-in var(--stream-fade-ms,240ms) cubic-bezier(.2,.7,.2,1) both;will-change:opacity;}
|
||||
@keyframes stream-fade-word-in{0%{opacity:0;}45%{opacity:.45;}100%{opacity:1;}}
|
||||
.stream-fade-word.is-new{animation:stream-fade-word-in var(--stream-fade-ms,620ms) cubic-bezier(.16,.84,.32,1) both;will-change:opacity;}
|
||||
@keyframes stream-fade-word-in{0%{opacity:0;}35%{opacity:.18;}70%{opacity:.72;}100%{opacity:1;}}
|
||||
@media (prefers-reduced-motion: reduce){.stream-fade-word.is-new{animation:none;will-change:auto;}}
|
||||
[data-live-assistant="1"]:last-child .msg-body.stream-fade-active > :last-child::after,
|
||||
[data-live-assistant="1"]:last-child .msg-body.stream-fade-active:not(:has(> *))::after{display:none;content:none;}
|
||||
|
||||
+86
-8
@@ -11372,8 +11372,7 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){
|
||||
el.hidden=true;
|
||||
});
|
||||
const liveFooter=blocks.querySelector('#liveRunStatus');
|
||||
let wrote=false;
|
||||
const targetRenderedRows=[];
|
||||
const renderedRows=[];
|
||||
for(const row of rows){
|
||||
const node=_anchorSceneTransparentNodeForRow(row,{
|
||||
live:true,
|
||||
@@ -11389,8 +11388,7 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){
|
||||
: node;
|
||||
if(existing) preserveByKey.delete(key);
|
||||
if(!renderedNode) continue;
|
||||
targetRenderedRows.push(renderedNode);
|
||||
wrote=true;
|
||||
renderedRows.push(renderedNode);
|
||||
}
|
||||
const transparentLiveRowAlreadyPositioned=(node, expectedNextSibling)=>!!(
|
||||
node &&
|
||||
@@ -11398,8 +11396,8 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){
|
||||
node.nextSibling===expectedNextSibling
|
||||
);
|
||||
let expectedNextSibling=(liveFooter&&liveFooter.parentElement===blocks) ? liveFooter : null;
|
||||
for(let i=targetRenderedRows.length-1;i>=0;i--){
|
||||
const renderedNode=targetRenderedRows[i];
|
||||
for(let i=renderedRows.length-1;i>=0;i--){
|
||||
const renderedNode=renderedRows[i];
|
||||
if(transparentLiveRowAlreadyPositioned(renderedNode,expectedNextSibling)){
|
||||
expectedNextSibling=renderedNode;
|
||||
continue;
|
||||
@@ -11409,7 +11407,7 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){
|
||||
expectedNextSibling=renderedNode;
|
||||
}
|
||||
preserveByKey.forEach(stale=>stale.remove());
|
||||
if(wrote) _syncTransparentEventControls(turn);
|
||||
if(renderedRows.length) _syncTransparentEventControls(turn);
|
||||
if(typeof _moveLiveRunStatusToTurnEnd==='function') _moveLiveRunStatusToTurnEnd();
|
||||
_restoreMessageScrollSnapshotSameFrame(scrollSnapshot);
|
||||
if(scrollRebuildGuard&&scrollRebuildGuard.release){
|
||||
@@ -11419,7 +11417,7 @@ function _renderLiveAnchorActivitySceneTransparent(streamId, scene, opts){
|
||||
});
|
||||
}
|
||||
if(!scrollRebuildGuard.readerAwayFromBottom&&typeof scrollIfPinned==='function') scrollIfPinned();
|
||||
return wrote;
|
||||
return !!renderedRows.length;
|
||||
}
|
||||
|
||||
function _transparentLiveRowKey(node, streamId){
|
||||
@@ -11514,10 +11512,90 @@ function _refreshTransparentThinkingLiveRow(existing, node){
|
||||
return true;
|
||||
}
|
||||
|
||||
function _bindTransparentFadeCleanup(body){
|
||||
if(!body || body._transparentFadeCleanupBound || typeof body.addEventListener !== 'function') return;
|
||||
body._transparentFadeCleanupBound = true;
|
||||
body.addEventListener('animationend', e=>{
|
||||
const span = e.target;
|
||||
if(!span || !span.classList || !span.classList.contains('stream-fade-word')) return;
|
||||
span.replaceWith(document.createTextNode(span.textContent || ''));
|
||||
});
|
||||
}
|
||||
|
||||
function _appendTransparentFadeText(body, text){
|
||||
if(!body) return;
|
||||
const value = String(text || '');
|
||||
if(!value) return;
|
||||
_bindTransparentFadeCleanup(body);
|
||||
const reduceMotion = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||||
const frag = document.createDocumentFragment();
|
||||
const wordRe = /(\S+)(\s*)/g;
|
||||
let last = 0, match, changed = false;
|
||||
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';
|
||||
span.textContent = match[1];
|
||||
frag.appendChild(span);
|
||||
}
|
||||
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)));
|
||||
body.appendChild(frag);
|
||||
}
|
||||
|
||||
function _refreshTransparentFadeProseRow(existing, node, preservedState){
|
||||
let body = existing.querySelector ? existing.querySelector('.msg-body') : null;
|
||||
const nextText = String((node.dataset && node.dataset.rawText) || (node.textContent || ''));
|
||||
const currentText = String(existing.getAttribute('data-stream-fade-text') || (body && body.textContent) || '');
|
||||
const pairs = _transparentLiveRowAttributePairs(node);
|
||||
const kept = Object.create(null);
|
||||
for(const pair of pairs){
|
||||
const [name, value] = pair;
|
||||
kept[String(name)] = String(value ?? '');
|
||||
}
|
||||
for(const [name] of _transparentLiveRowAttributePairs(existing)){
|
||||
if(!Object.prototype.hasOwnProperty.call(kept, name)) existing.removeAttribute(name);
|
||||
}
|
||||
for(const pair of pairs){
|
||||
const [name, value] = pair;
|
||||
existing.setAttribute(name, value);
|
||||
}
|
||||
existing.className = node.className || '';
|
||||
if(!body){
|
||||
body = document.createElement('div');
|
||||
body.className = 'msg-body';
|
||||
existing.appendChild(body);
|
||||
}
|
||||
if(body.classList) body.classList.add('stream-fade-active');
|
||||
if(!nextText.startsWith(currentText)){
|
||||
body.textContent = '';
|
||||
existing.setAttribute('data-stream-fade-text', '');
|
||||
_appendTransparentFadeText(body, nextText);
|
||||
}else{
|
||||
_appendTransparentFadeText(body, nextText.slice(currentText.length));
|
||||
}
|
||||
existing.setAttribute('data-stream-fade-text', nextText);
|
||||
_rehydrateTransparentLiveRow(existing, node, preservedState);
|
||||
return existing;
|
||||
}
|
||||
|
||||
function _refreshTransparentLiveRow(existing, node){
|
||||
if(!existing || !node || !existing.getAttribute) return node;
|
||||
if(existing===node) return existing;
|
||||
const preservedState = _transparentLiveRowInteractiveState(existing);
|
||||
const candidateIsFadeProse = node.getAttribute('data-anchor-row-role') === 'prose' &&
|
||||
node.querySelector &&
|
||||
!!node.querySelector('.msg-body.stream-fade-active,.stream-fade-word');
|
||||
if(candidateIsFadeProse){
|
||||
return _refreshTransparentFadeProseRow(existing, node, preservedState);
|
||||
}
|
||||
const pairs = _transparentLiveRowAttributePairs(node);
|
||||
const kept = Object.create(null);
|
||||
for(const pair of pairs){
|
||||
|
||||
@@ -55,6 +55,7 @@ function extractFunc(name){{
|
||||
return src.slice(start, i);
|
||||
}}
|
||||
class FakeElement {{
|
||||
static moves = 0;
|
||||
constructor(tag='div'){{
|
||||
this.tagName = String(tag).toUpperCase();
|
||||
this.children = [];
|
||||
@@ -143,7 +144,15 @@ class FakeElement {{
|
||||
}}
|
||||
appendChild(child){{
|
||||
this.appendChildCount = (this.appendChildCount || 0) + 1;
|
||||
if(child && child.parentNode) child.remove();
|
||||
if(child && child.tagName === '#FRAGMENT'){{
|
||||
child.children.slice().forEach(grandchild=>this.appendChild(grandchild));
|
||||
child.children = [];
|
||||
return child;
|
||||
}}
|
||||
if(child && child.parentNode){{
|
||||
FakeElement.moves += 1;
|
||||
child.remove();
|
||||
}}
|
||||
if(!child) return null;
|
||||
child.parentNode = this;
|
||||
this.children.push(child);
|
||||
@@ -151,7 +160,10 @@ class FakeElement {{
|
||||
}}
|
||||
insertBefore(child, refNode){{
|
||||
this.insertBeforeCount = (this.insertBeforeCount || 0) + 1;
|
||||
if(child && child.parentNode) child.remove();
|
||||
if(child && child.parentNode){{
|
||||
FakeElement.moves += 1;
|
||||
child.remove();
|
||||
}}
|
||||
if(!child) return null;
|
||||
const idx = this.children.indexOf(refNode);
|
||||
child.parentNode = this;
|
||||
@@ -218,7 +230,15 @@ function matchesSimple(el, selector){{
|
||||
}}
|
||||
|
||||
global.window = {{}};
|
||||
global.document = {{ createElement:(tag)=>new FakeElement(tag) }};
|
||||
global.document = {{
|
||||
createElement:(tag)=>new FakeElement(tag),
|
||||
createTextNode:(text)=>{{
|
||||
const node = new FakeElement('#text');
|
||||
node.textContent = text;
|
||||
return node;
|
||||
}},
|
||||
createDocumentFragment:()=>new FakeElement('#fragment'),
|
||||
}};
|
||||
global.CSS = {{ escape:(value)=>String(value) }};
|
||||
global.requestAnimationFrame = (fn)=>fn();
|
||||
|
||||
@@ -286,6 +306,9 @@ eval(extractFunc('_transparentLiveRowAttributePairs'));
|
||||
eval(extractFunc('_transparentLiveRowInteractiveState'));
|
||||
eval(extractFunc('_rehydrateTransparentLiveRow'));
|
||||
eval(extractFunc('_refreshTransparentThinkingLiveRow'));
|
||||
eval(extractFunc('_bindTransparentFadeCleanup'));
|
||||
eval(extractFunc('_appendTransparentFadeText'));
|
||||
eval(extractFunc('_refreshTransparentFadeProseRow'));
|
||||
eval(extractFunc('_refreshTransparentLiveRow'));
|
||||
eval(extractFunc('_renderLiveAnchorActivitySceneTransparent'));
|
||||
|
||||
@@ -330,6 +353,9 @@ const keptAfterSecond = turn.querySelector('.transparent-event-row[data-anchor-r
|
||||
const staleAfterSecond = turn.querySelector('.transparent-event-row[data-anchor-row-id=\"row-stale\"]');
|
||||
const newAfterSecond = turn.querySelector('.transparent-event-row[data-anchor-row-id=\"row-new\"]');
|
||||
const rows = turn.children.filter((child) => child.classList.contains('transparent-event-row'));
|
||||
const movesBeforeStableRender = FakeElement.moves;
|
||||
const stableOrderRender = _renderLiveAnchorActivitySceneTransparent('stream-1', secondScene, {{ sessionId:'session-1' }});
|
||||
const movesAfterStableRender = FakeElement.moves;
|
||||
const idxs = {{
|
||||
keptDirect: turn.children.indexOf(keptAfterSecond),
|
||||
freshDirect: turn.children.indexOf(newAfterSecond),
|
||||
@@ -378,10 +404,46 @@ const keylessRowsAfterThird = turn.querySelectorAll('.transparent-event-row[data
|
||||
_renderLiveAnchorActivitySceneTransparent('stream-1', fourthScene, {{ sessionId:'session-1' }});
|
||||
const keylessRowsAfterFourth = turn.querySelectorAll('.transparent-event-row[data-anchor-row-id=\"\"]');
|
||||
|
||||
const fadeParent = new FakeElement('div');
|
||||
const fadeExisting = new FakeElement('div');
|
||||
fadeExisting.className = 'assistant-segment transparent-event-row';
|
||||
fadeExisting.setAttribute('data-anchor-row-role', 'prose');
|
||||
fadeExisting.setAttribute('data-anchor-row-id', 'fade-row');
|
||||
fadeExisting.setAttribute('data-anchor-source-event-type', 'process_prose');
|
||||
const staleBody = new FakeElement('div');
|
||||
staleBody.className = 'msg-body stream-fade-active';
|
||||
const oldSpan = new FakeElement('span');
|
||||
oldSpan.className = 'stream-fade-word is-new';
|
||||
oldSpan.textContent = 'old';
|
||||
staleBody.appendChild(oldSpan);
|
||||
staleBody.appendChild((()=>{{ const n = new FakeElement('#text'); n.textContent = ' '; return n; }})());
|
||||
fadeExisting.appendChild(staleBody);
|
||||
fadeExisting.setAttribute('data-stream-fade-text', 'old ');
|
||||
fadeParent.appendChild(fadeExisting);
|
||||
|
||||
const fadeCandidate = new FakeElement('div');
|
||||
fadeCandidate.className = 'assistant-segment transparent-event-row';
|
||||
fadeCandidate.setAttribute('data-anchor-row-role', 'prose');
|
||||
fadeCandidate.setAttribute('data-anchor-row-id', 'fade-row');
|
||||
fadeCandidate.setAttribute('data-anchor-source-event-type', 'process_prose');
|
||||
fadeCandidate.dataset.rawText = 'old new';
|
||||
const fadeBody = new FakeElement('div');
|
||||
fadeBody.className = 'msg-body stream-fade-active';
|
||||
const fadeSpan = new FakeElement('span');
|
||||
fadeSpan.className = 'stream-fade-word is-new';
|
||||
fadeSpan.textContent = 'old';
|
||||
fadeBody.appendChild(fadeSpan);
|
||||
fadeBody.appendChild((()=>{{ const n = new FakeElement('#text'); n.textContent = ' new'; return n; }})());
|
||||
fadeCandidate.appendChild(fadeBody);
|
||||
const fadeRefresh = _refreshTransparentLiveRow(fadeExisting, fadeCandidate);
|
||||
const fadeSpans = fadeParent.querySelectorAll('.stream-fade-word.is-new');
|
||||
|
||||
process.stdout.write(JSON.stringify({{
|
||||
firstRender,
|
||||
secondRender,
|
||||
stableOrderRender,
|
||||
sameNode: keptAfterFirst === keptAfterSecond,
|
||||
stableOrderMovedRows: movesAfterStableRender - movesBeforeStableRender,
|
||||
keptId: keptAfterSecond && keptAfterSecond.getAttribute('data-anchor-row-id'),
|
||||
keptSource: keptAfterSecond && keptAfterSecond.getAttribute('data-anchor-source-event-type'),
|
||||
keptText: keptAfterSecond && keptAfterSecond.textContent,
|
||||
@@ -402,13 +464,23 @@ process.stdout.write(JSON.stringify({{
|
||||
keylessAfterFourth: keylessRowsAfterFourth.length,
|
||||
keylessTextsAfterFourth: keylessRowsAfterFourth.map((child) => child.textContent),
|
||||
totalRowsAfterFourth: turn.children.filter((child) => child.classList.contains('transparent-event-row')).length,
|
||||
fadeKeptExisting: fadeRefresh === fadeExisting,
|
||||
fadeExistingStillParented: fadeExisting.parentNode === fadeParent,
|
||||
fadeCandidateDetached: fadeCandidate.parentNode === null,
|
||||
fadeOldSpanPreserved: fadeSpans[0] === oldSpan,
|
||||
fadeSpanTexts: fadeSpans.map(span=>span.textContent),
|
||||
fadeText: fadeExisting.getAttribute('data-stream-fade-text'),
|
||||
fadeSpanPreserved: !!fadeParent.querySelector('.stream-fade-word.is-new'),
|
||||
fadeBodyActive: !!fadeParent.querySelector('.msg-body.stream-fade-active'),
|
||||
}}));
|
||||
"""
|
||||
script = script.replace("{{", "{").replace("}}", "}")
|
||||
data = _run_node_script(script, str(ROOT / "static" / "ui.js"))
|
||||
assert data["firstRender"] is True
|
||||
assert data["secondRender"] is True
|
||||
assert data["stableOrderRender"] is True
|
||||
assert data["sameNode"] is True
|
||||
assert data["stableOrderMovedRows"] == 0
|
||||
assert data["keptId"] == "row-kept"
|
||||
assert data["keptSource"] == "process_prose"
|
||||
assert data["keptText"] == "updated progress line"
|
||||
@@ -438,6 +510,14 @@ process.stdout.write(JSON.stringify({{
|
||||
assert data["keylessAfterFourth"] == 1
|
||||
assert data["keylessTextsAfterFourth"] == ["keyless row second"]
|
||||
assert data["totalRowsAfterFourth"] == 1
|
||||
assert data["fadeKeptExisting"] is True
|
||||
assert data["fadeExistingStillParented"] is True
|
||||
assert data["fadeCandidateDetached"] is True
|
||||
assert data["fadeOldSpanPreserved"] is True
|
||||
assert data["fadeSpanTexts"] == ["old", "new"]
|
||||
assert data["fadeText"] == "old new"
|
||||
assert data["fadeSpanPreserved"] is True
|
||||
assert data["fadeBodyActive"] is True
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
|
||||
@@ -306,7 +306,8 @@ def test_process_prose_is_an_anchor_scene_row_not_a_dom_mirror():
|
||||
assert "_upsertAnchorProcessProse(displayText,{sealed:force})" in flush
|
||||
assert "function _upsertAnchorProcessProse" in MESSAGES_JS
|
||||
assert "source_event_type:sourceEventType" in _function_body(MESSAGES_JS, "_applyToAnchor")
|
||||
assert "_upsertAnchorProcessProse(displayText)" in schedule
|
||||
assert "let anchorProcessText=displayText" in schedule
|
||||
assert "_upsertAnchorProcessProse(anchorProcessText)" in schedule
|
||||
assert "function _replaceAnchorActivityEventByLocalId" in MESSAGES_JS
|
||||
assert "events[i]=next" in MESSAGES_JS
|
||||
assert "_renderAnchorLiveScene();" in _function_body(MESSAGES_JS, "_upsertAnchorProcessProse")
|
||||
|
||||
+223
-13
@@ -82,15 +82,14 @@ let _streamFadeLastTargetWords=0;
|
||||
let _streamFadeLastArrivalMs=0;
|
||||
let _streamFadeArrivalWps=0;
|
||||
let _streamFadeLatestAnimationEndAt=0;
|
||||
let _streamFadeAppendOffset=0;
|
||||
let _streamFadeVisibleWords=0;
|
||||
let _streamFadeHoldUntilMs=0;
|
||||
let _streamFadeCurrentMs=200;
|
||||
const _STREAM_FADE_MS=200;
|
||||
const _STREAM_FADE_MAX_MS=350;
|
||||
const _STREAM_FADE_STAGGER_MS=16;
|
||||
const _STREAM_FADE_DONE_MAX_MS=320;
|
||||
const _STREAM_FADE_DONE_DRAIN_MAX_MS=900;
|
||||
let _streamFadeCurrentMs=620;
|
||||
let _streamFadeDomText='';
|
||||
const _STREAM_FADE_MS=620;
|
||||
const _STREAM_FADE_MAX_MS=900;
|
||||
const _STREAM_FADE_DONE_MAX_MS=1000;
|
||||
const _STREAM_FADE_DONE_DRAIN_MAX_MS=1400;
|
||||
const performance={performance_stub};
|
||||
{helpers}
|
||||
"""
|
||||
@@ -166,12 +165,30 @@ def test_stream_fade_uses_incremental_renderer_without_changing_default_path():
|
||||
[
|
||||
"_streamFadeNextText(displayText)",
|
||||
"if(!next.changed) return next.caughtUp",
|
||||
"if(!_shouldUseTransparentStreamFade())",
|
||||
"_smdNewParser(assistantBody,true)",
|
||||
"_smdWrite(next.text,true)",
|
||||
"_sanitizeSmdLinks(assistantBody)",
|
||||
"assistantBody.appendChild(document.createTextNode(delta))",
|
||||
"_streamFadeDomText=String(next.text||'')",
|
||||
"stream-fade-active",
|
||||
],
|
||||
)
|
||||
assert "renderMd ? renderMd(next.text||'')" in render_block
|
||||
assert render_block.index("_smdWrite(next.text,true)") < render_block.index(
|
||||
"assistantBody.appendChild(document.createTextNode(delta))"
|
||||
)
|
||||
assert "_streamFadeAppendText(assistantBody,delta)" not in render_block
|
||||
assert "_streamFadeBindCleanup(assistantBody)" 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,
|
||||
[
|
||||
@@ -189,20 +206,211 @@ def test_stream_fade_uses_incremental_renderer_without_changing_default_path():
|
||||
["animationend", "span.replaceWith(document.createTextNode"],
|
||||
)
|
||||
assert "_wrapStreamingFadeWords" not in MESSAGES_JS
|
||||
assert "animationDelay" not in renderer_block
|
||||
assert "_STREAM_FADE_STAGGER_MS" not in MESSAGES_JS
|
||||
assert "_streamFadeAppendOffset" 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=620;
|
||||
let _streamFadeLatestAnimationEndAt=0;
|
||||
let _streamFadeCurrentMs=620;
|
||||
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")
|
||||
predicate_block = function_block(MESSAGES_JS, "_shouldUseLiveProseFade")
|
||||
assert_contains_all(
|
||||
anchor_block,
|
||||
[
|
||||
"const fade=typeof _shouldUseLiveProseFade==='function'&&_shouldUseLiveProseFade()",
|
||||
"if(st && st.fade!==fade) st=null",
|
||||
"if(body.classList) body.classList.toggle('stream-fade-active',fade)",
|
||||
"const baseRenderer=fade?_streamFadeRenderer(body):_safeSmdRenderer(body)",
|
||||
"st={node,parser:window.smd.parser(renderer),writtenText:'',fade}",
|
||||
"const body=st.node&&st.node.querySelector&&st.node.querySelector('.msg-body')",
|
||||
],
|
||||
)
|
||||
assert_contains_all(
|
||||
predicate_block,
|
||||
[
|
||||
"!_streamFadeReduceMotionEnabled()",
|
||||
"_shouldUseStreamFade()",
|
||||
"_shouldUseTransparentStreamFade()",
|
||||
],
|
||||
)
|
||||
assert "function _shouldUseTransparentStreamFade()" in MESSAGES_JS
|
||||
assert "typeof isTransparentStream==='function'&&isTransparentStream()" in MESSAGES_JS
|
||||
|
||||
|
||||
def test_reduced_motion_disables_live_prose_fade_predicate():
|
||||
script = (
|
||||
"\n".join(
|
||||
function_block(MESSAGES_JS, name)
|
||||
for name in [
|
||||
"_shouldUseStreamFade",
|
||||
"_shouldUseTransparentStreamFade",
|
||||
"_streamFadeReduceMotionEnabled",
|
||||
"_shouldUseLiveProseFade",
|
||||
]
|
||||
)
|
||||
+ r"""
|
||||
let _streamFadeReduceMotionMql=null;
|
||||
let _streamFadeReduceMotion=false;
|
||||
let _streamFadeReduceMotionOnChange=null;
|
||||
let transparent=true;
|
||||
let reduceMotion=true;
|
||||
global.window={
|
||||
_fadeTextEffect:true,
|
||||
matchMedia(){
|
||||
return {
|
||||
get matches(){ return reduceMotion; },
|
||||
addEventListener(){},
|
||||
removeEventListener(){},
|
||||
};
|
||||
},
|
||||
};
|
||||
function isTransparentStream(){ return transparent; }
|
||||
if(_shouldUseLiveProseFade()) throw new Error('reduced motion allowed live prose fade');
|
||||
_streamFadeReduceMotionMql=null;
|
||||
reduceMotion=false;
|
||||
window._fadeTextEffect=false;
|
||||
if(!_shouldUseLiveProseFade()) throw new Error('transparent stream fade should work when motion is allowed');
|
||||
_streamFadeReduceMotionMql=null;
|
||||
transparent=false;
|
||||
window._fadeTextEffect=true;
|
||||
if(!_shouldUseLiveProseFade()) throw new Error('regular fade preference should work when motion is allowed');
|
||||
"""
|
||||
)
|
||||
run_node(script)
|
||||
|
||||
|
||||
def test_transparent_stream_hidden_body_appends_plain_text_only():
|
||||
script = (
|
||||
function_block(MESSAGES_JS, "_renderStreamingFadeMarkdown")
|
||||
+ r"""
|
||||
let _streamFadeDomText='';
|
||||
let _smdParser=null;
|
||||
let _smdReconnect=false;
|
||||
let parserEnded=false;
|
||||
function _streamFadeNextText(){ return {changed:true,caughtUp:false,text:'alpha beta'}; }
|
||||
function _shouldUseTransparentStreamFade(){ return true; }
|
||||
function _smdEndParser(){ parserEnded=true; }
|
||||
const assistantBody={
|
||||
textContent:'',
|
||||
innerHTML:'',
|
||||
children:[],
|
||||
classList:{added:[],add(name){ this.added.push(name); }},
|
||||
appendChild(node){
|
||||
this.children.push(node);
|
||||
this.textContent += String(node.textContent || '');
|
||||
return node;
|
||||
},
|
||||
};
|
||||
global.document={
|
||||
createTextNode(text){ return {type:'text',textContent:String(text)}; },
|
||||
};
|
||||
const caughtUp=_renderStreamingFadeMarkdown('alpha beta');
|
||||
if(caughtUp) throw new Error('expected fade playout to remain catching up');
|
||||
if(assistantBody.textContent!=='alpha beta') throw new Error(`wrong hidden text: ${assistantBody.textContent}`);
|
||||
if(_streamFadeDomText!=='alpha beta') throw new Error(`wrong dom text: ${_streamFadeDomText}`);
|
||||
if(assistantBody.children.some(node=>node.className==='stream-fade-word is-new')){
|
||||
throw new Error('hidden body received fade span');
|
||||
}
|
||||
if(!assistantBody.classList.added.includes('stream-fade-active')) throw new Error('missing stream fade active marker');
|
||||
"""
|
||||
)
|
||||
run_node(script)
|
||||
|
||||
|
||||
def test_transparent_anchor_prose_receives_revealed_fade_text():
|
||||
render_section = slice_between(
|
||||
MESSAGES_JS,
|
||||
"const displayText = segmentStart===0",
|
||||
"scrollIfPinned();",
|
||||
)
|
||||
assert_contains_all(
|
||||
render_section,
|
||||
[
|
||||
"let anchorProcessText=displayText",
|
||||
"if(assistantBody){",
|
||||
"const caughtUp=_renderStreamingFadeMarkdown(displayText)",
|
||||
"if(_shouldUseLiveProseFade())",
|
||||
"anchorProcessText=_streamFadeDomText||''",
|
||||
"if(anchorProcessText) _upsertAnchorProcessProse(anchorProcessText)",
|
||||
],
|
||||
)
|
||||
assert render_section.index("let anchorProcessText=displayText") < render_section.index("if(assistantBody){")
|
||||
assert render_section.index("anchorProcessText=_streamFadeDomText||''") < render_section.index(
|
||||
"_upsertAnchorProcessProse(anchorProcessText)"
|
||||
)
|
||||
assert render_section.index("if(assistantBody){") < render_section.rindex(
|
||||
"if(anchorProcessText) _upsertAnchorProcessProse(anchorProcessText)"
|
||||
)
|
||||
|
||||
|
||||
def test_stream_fade_done_drain_has_hard_cap_for_large_buffered_responses():
|
||||
drain_block = function_block(MESSAGES_JS, "_drainStreamFadeBeforeDone")
|
||||
assert "const _STREAM_FADE_DONE_DRAIN_MAX_MS=900" in MESSAGES_JS
|
||||
assert "const _STREAM_FADE_DONE_DRAIN_MAX_MS=1400" in MESSAGES_JS
|
||||
assert_contains_all(
|
||||
drain_block,
|
||||
[
|
||||
"const drainStartedAt=performance.now();",
|
||||
"const target=_streamFadeCurrentDisplayText();",
|
||||
"const caughtUp=_renderStreamingFadeMarkdown(target);",
|
||||
"const anchorProcessText=_streamFadeDomText||target;",
|
||||
"if(anchorProcessText) _upsertAnchorProcessProse(anchorProcessText);",
|
||||
"performance.now()-drainStartedAt>=_STREAM_FADE_DONE_DRAIN_MAX_MS",
|
||||
"if(_smdParser) _smdEndParser();",
|
||||
"onDone();",
|
||||
],
|
||||
)
|
||||
assert drain_block.index("_renderStreamingFadeMarkdown(target)") < drain_block.index(
|
||||
"_upsertAnchorProcessProse(anchorProcessText)"
|
||||
)
|
||||
|
||||
|
||||
def test_live_streaming_assistant_content_opts_out_of_global_theme_transitions():
|
||||
@@ -247,7 +455,9 @@ def test_stream_fade_css_is_opacity_only_and_hides_live_cursor():
|
||||
[
|
||||
"@keyframes stream-fade-word-in",
|
||||
".stream-fade-word.is-new",
|
||||
"var(--stream-fade-ms,240ms) cubic-bezier(.2,.7,.2,1)",
|
||||
"var(--stream-fade-ms,620ms) cubic-bezier(.16,.84,.32,1)",
|
||||
"35%{opacity:.18;}",
|
||||
"70%{opacity:.72;}",
|
||||
"prefers-reduced-motion: reduce",
|
||||
".msg-body.stream-fade-active > :last-child::after",
|
||||
"display:none",
|
||||
@@ -273,12 +483,12 @@ const words=Array.from({length:260},(_,i)=>'w'+i).join(' ');
|
||||
performance._t += 33;
|
||||
let out=_streamFadeNextText('slow start');
|
||||
if(!out.changed) throw new Error('expected initial reveal');
|
||||
if(_streamFadeCurrentMs !== 200) throw new Error(`expected base fade 200ms, got ${_streamFadeCurrentMs}`);
|
||||
for(let frame=0;frame<20&&_streamFadeCurrentMs<350;frame++){
|
||||
if(_streamFadeCurrentMs !== 620) throw new Error(`expected base fade 620ms, got ${_streamFadeCurrentMs}`);
|
||||
for(let frame=0;frame<20&&_streamFadeCurrentMs<900;frame++){
|
||||
performance._t += 120;
|
||||
out=_streamFadeNextText(words);
|
||||
}
|
||||
if(_streamFadeCurrentMs !== 350) throw new Error(`expected max fade 350ms, got ${_streamFadeCurrentMs}`);
|
||||
if(_streamFadeCurrentMs !== 900) throw new Error(`expected max fade 900ms, got ${_streamFadeCurrentMs}`);
|
||||
"""
|
||||
)
|
||||
run_node(script)
|
||||
|
||||
Reference in new issue
Block a user