@@ -12,6 +12,16 @@ const State = {
1212 pythonPath : null ,
1313 outputDir : null ,
1414 dataDir : null ,
15+ // Persisted Pipeline page form values (survive navigation)
16+ pipeline : {
17+ courseId : '' ,
18+ detail : '7' ,
19+ courseName : '' ,
20+ lecFilter : '' ,
21+ stealth : false ,
22+ force : false ,
23+ steps : { mat : true , vid : true , trans : true , align : true , gen : true } ,
24+ } ,
1525} ;
1626
1727// ── Snackbar ──────────────────────────────────────────────────────────────────
@@ -31,11 +41,12 @@ function snack(msg, ok = true) {
3141
3242// ── Terminal ──────────────────────────────────────────────────────────────────
3343const Term = ( ( ) => {
34- const MAX_LINES = 500 ;
44+ const MAX_LINES = 2000 ;
3545 const ANSI_RE = / \x1b \[ [ 0 - 9 ; ] * [ m A B C D E F G H J K S T f r ] | \x1b \] [ ^ \x07 ] * \x07 | \x1b [ ( ) ] [ A B 0 1 2 ] / g;
36- let lines = [ ] ; // array of {text, cls}
37- let currentText = '' ; // current line buffer
46+ let lines = [ ] ; // array of {text, cls} — committed (newline-terminated)
47+ let currentText = '' ; // current partial line buffer
3848 let scheduledRender = false ;
49+ let renderedCount = 0 ; // lines[0..renderedCount-1] are already in the DOM
3950
4051 function classify ( text ) {
4152 const t = text . toLowerCase ( ) . trimStart ( ) ;
@@ -57,21 +68,43 @@ const Term = (() => {
5768 }
5869
5970 function render ( ) {
60- const el = document . getElementById ( 'terminal-output' ) ;
71+ const el = document . getElementById ( 'terminal-output' ) ;
6172 if ( ! el ) return ;
6273 const atBottom = el . scrollHeight - el . clientHeight - el . scrollTop < 30 ;
63- const frag = document . createDocumentFragment ( ) ;
64- // Current partial line
65- const allLines = [ ...lines , { text : currentText , cls : '' } ] ;
66- // Only re-render last N if performance is a concern; here we rebuild all
67- for ( const { text, cls } of allLines ) {
68- const div = document . createElement ( 'div' ) ;
69- div . className = 'term-line' + ( cls ? ' ' + cls : '' ) ;
70- div . textContent = text ;
71- frag . appendChild ( div ) ;
74+
75+ // If the DOM was wiped (after clear()), reset tracked count
76+ if ( el . children . length === 0 ) renderedCount = 0 ;
77+
78+ // Append only newly committed lines (don't touch existing DOM nodes)
79+ if ( renderedCount < lines . length ) {
80+ const frag = document . createDocumentFragment ( ) ;
81+ for ( let i = renderedCount ; i < lines . length ; i ++ ) {
82+ const { text, cls } = lines [ i ] ;
83+ const div = document . createElement ( 'div' ) ;
84+ div . className = 'term-line' + ( cls ? ' ' + cls : '' ) ;
85+ div . textContent = text ;
86+ frag . appendChild ( div ) ;
87+ }
88+ // Insert before the partial-line placeholder if it exists
89+ const partDiv = el . lastChild ;
90+ if ( partDiv && partDiv . _isPartial ) {
91+ el . insertBefore ( frag , partDiv ) ;
92+ } else {
93+ el . appendChild ( frag ) ;
94+ }
95+ renderedCount = lines . length ;
96+ }
97+
98+ // Update the partial-line placeholder in-place (no flicker)
99+ let partDiv = el . lastChild ;
100+ if ( ! partDiv || ! partDiv . _isPartial ) {
101+ partDiv = document . createElement ( 'div' ) ;
102+ partDiv . className = 'term-line' ;
103+ partDiv . _isPartial = true ;
104+ el . appendChild ( partDiv ) ;
72105 }
73- el . innerHTML = '' ;
74- el . appendChild ( frag ) ;
106+ partDiv . textContent = currentText ;
107+
75108 if ( atBottom ) el . scrollTop = el . scrollHeight ;
76109 }
77110
@@ -84,7 +117,10 @@ const Term = (() => {
84117 const t = currentText ;
85118 if ( ! t . startsWith ( '<frozen importlib' ) ) {
86119 lines . push ( { text : t , cls : classify ( t ) } ) ;
87- if ( lines . length > MAX_LINES ) lines . shift ( ) ;
120+ if ( lines . length > MAX_LINES ) {
121+ lines . shift ( ) ;
122+ renderedCount = Math . max ( 0 , renderedCount - 1 ) ;
123+ }
88124 }
89125 currentText = '' ;
90126 } else {
@@ -97,15 +133,20 @@ const Term = (() => {
97133 function write ( text , cls = '' ) {
98134 for ( const line of text . split ( '\n' ) ) {
99135 lines . push ( { text : line , cls } ) ;
100- if ( lines . length > MAX_LINES ) lines . shift ( ) ;
136+ if ( lines . length > MAX_LINES ) {
137+ lines . shift ( ) ;
138+ renderedCount = Math . max ( 0 , renderedCount - 1 ) ;
139+ }
101140 }
102141 scheduleRender ( ) ;
103142 }
104143
105144 function clear ( ) {
106145 lines = [ ] ;
107146 currentText = '' ;
108- render ( ) ;
147+ renderedCount = 0 ;
148+ const el = document . getElementById ( 'terminal-output' ) ;
149+ if ( el ) el . innerHTML = '' ;
109150 setStatus ( '' ) ;
110151 }
111152
@@ -153,13 +194,14 @@ function statusChip(done, total) {
153194 return chip ( `◐ ${ done } /${ total } ` , 'chip-warn' ) ;
154195}
155196
156- function courseOptions ( includeAll = false ) {
197+ function courseOptions ( includeAll = false , selectedId = '' ) {
157198 if ( ! State . courses . length ) {
158199 return `<option value="">— no courses, add Canvas token in Settings —</option>` ;
159200 }
160- let html = includeAll ? `<option value="0">All courses</option>` : '' ;
201+ const sel = v => String ( v ) === String ( selectedId ) ? ' selected' : '' ;
202+ let html = includeAll ? `<option value="0"${ sel ( '0' ) } >All courses</option>` : '' ;
161203 for ( const c of State . courses ) {
162- html += `<option value="${ c . id } ">${ esc ( c . name ) } (${ c . id } )</option>` ;
204+ html += `<option value="${ c . id } "${ sel ( c . id ) } >${ esc ( c . name ) } (${ c . id } )</option>` ;
163205 }
164206 return html ;
165207}
@@ -252,7 +294,13 @@ function runChain(steps) {
252294 window . api . offProcessEvents ( ) ;
253295
254296 function runNext ( ) {
255- if ( idx >= steps . length ) return ;
297+ if ( idx >= steps . length ) {
298+ State . running = false ;
299+ Term . showStop ( false ) ;
300+ Term . write ( '\n✓ All steps completed successfully.' , 'ok' ) ;
301+ Term . setStatus ( '✓ done' , 'var(--c-success)' ) ;
302+ return ;
303+ }
256304 const [ label , cmd ] = steps [ idx ++ ] ;
257305 const stepLabel = `Step ${ idx } /${ steps . length } : ${ label } ` ;
258306 Term . write ( `\n${ '─' . repeat ( 50 ) } \n▶ ${ stepLabel } \n` , 'cmd' ) ;
@@ -418,51 +466,52 @@ async function loadDashboardStats() {
418466
419467// ── Page: Full Pipeline ────────────────────────────────────────────────────────
420468function buildPipeline ( ) {
421- const defaultCid = firstCourseId ( ) ;
469+ const ps = State . pipeline ;
470+ const cid = ps . courseId || firstCourseId ( ) ;
422471 return `
423472 ${ sectionTitle ( 'Full Pipeline Wizard' , '' ) }
424473 ${ mkCard ( `
425474 <div class="row">
426475 <div class="col expand">
427476 <span class="label">Course</span>
428477 <select id="pp-course" class="select-ctrl">
429- ${ courseOptions ( ) }
478+ ${ courseOptions ( false , cid ) }
430479 </select>
431480 </div>
432481 </div>
433482 ` ) }
434483 <div class="row" style="align-items:flex-start">
435484 ${ mkCard ( `
436485 <span class="label">Steps to execute</span>
437- ${ mkCheckbox ( 'pp-step-mat' , 'Download materials' ) }
438- ${ mkCheckbox ( 'pp-step-vid' , 'Download videos' ) }
439- ${ mkCheckbox ( 'pp-step-trans' , 'Transcribe videos' ) }
440- ${ mkCheckbox ( 'pp-step-align' , 'Align transcripts' ) }
441- ${ mkCheckbox ( 'pp-step-gen' , 'Generate study notes' ) }
486+ ${ mkCheckbox ( 'pp-step-mat' , 'Download materials' , ps . steps . mat ) }
487+ ${ mkCheckbox ( 'pp-step-vid' , 'Download videos' , ps . steps . vid ) }
488+ ${ mkCheckbox ( 'pp-step-trans' , 'Transcribe videos' , ps . steps . trans ) }
489+ ${ mkCheckbox ( 'pp-step-align' , 'Align transcripts' , ps . steps . align ) }
490+ ${ mkCheckbox ( 'pp-step-gen' , 'Generate study notes' , ps . steps . gen ) }
442491 ` , 'col expand' ) }
443492 ${ mkCard ( `
444493 <span class="label">Download</span>
445- ${ mkSwitch ( 'pp-stealth' , 'Stealth mode for downloads' ) }
494+ ${ mkSwitch ( 'pp-stealth' , 'Stealth mode for downloads' , ps . stealth ) }
446495 <hr class="divider">
447496 <span class="label">Note generation</span>
448497 <div class="field">
449498 <label class="label">Course name for notes</label>
450- <input id="pp-course-name" class="input-text" type="text" value="">
499+ <input id="pp-course-name" class="input-text" type="text" value="${ esc ( ps . courseName ) } ">
451500 </div>
452501 <div class="row center" style="gap:8px;margin-top:8px">
453- <span class="slider-value" id="pp-detail-val">7 </span>
502+ <span class="slider-value" id="pp-detail-val">${ esc ( ps . detail ) } </span>
454503 <div class="col expand">
455- <input id="pp-detail" type="range" min="0" max="10" value="7 " step="1" class="slider">
504+ <input id="pp-detail" type="range" min="0" max="10" value="${ esc ( ps . detail ) } " step="1" class="slider">
456505 <div style="font-size:10px;color:var(--c-white-35);margin-top:2px">
457506 0-2 Outline · 3-5 Bullets · 6-8 Paragraphs · 9-10 Exhaustive
458507 </div>
459508 </div>
460509 </div>
461510 <div class="field" style="margin-top:8px">
462511 <label class="label">Lecture filter</label>
463- <input id="pp-lec-filter" class="input-text" type="text" placeholder="1-5 or 1,3,5 (blank=all)">
512+ <input id="pp-lec-filter" class="input-text" type="text" value=" ${ esc ( ps . lecFilter ) } " placeholder="1-5 or 1,3,5 (blank=all)">
464513 </div>
465- ${ mkSwitch ( 'pp-force' , 'Force regenerate' ) }
514+ ${ mkSwitch ( 'pp-force' , 'Force regenerate' , ps . force ) }
466515 ` , 'col expand' ) }
467516 </div>
468517 <div class="row">
@@ -1127,7 +1176,48 @@ async function attachPageHandlers() {
11271176 if ( pg === 1 ) {
11281177 const detSlider = document . getElementById ( 'pp-detail' ) ;
11291178 const detLabel = document . getElementById ( 'pp-detail-val' ) ;
1130- detSlider ?. addEventListener ( 'input' , ( ) => { detLabel . textContent = detSlider . value ; } ) ;
1179+ detSlider ?. addEventListener ( 'input' , ( ) => {
1180+ detLabel . textContent = detSlider . value ;
1181+ State . pipeline . detail = detSlider . value ;
1182+ } ) ;
1183+
1184+ // Persist all form values so they survive navigation
1185+ document . getElementById ( 'pp-course' ) ?. addEventListener ( 'change' , e => {
1186+ State . pipeline . courseId = e . target . value ;
1187+ const nameEl = document . getElementById ( 'pp-course-name' ) ;
1188+ if ( nameEl && ! nameEl . value . trim ( ) ) {
1189+ nameEl . value = courseNameFromId ( e . target . value ) ;
1190+ State . pipeline . courseName = nameEl . value ;
1191+ }
1192+ } ) ;
1193+ document . getElementById ( 'pp-course-name' ) ?. addEventListener ( 'input' , e => {
1194+ State . pipeline . courseName = e . target . value ;
1195+ } ) ;
1196+ document . getElementById ( 'pp-lec-filter' ) ?. addEventListener ( 'input' , e => {
1197+ State . pipeline . lecFilter = e . target . value ;
1198+ } ) ;
1199+ document . getElementById ( 'pp-stealth' ) ?. addEventListener ( 'change' , e => {
1200+ State . pipeline . stealth = e . target . checked ;
1201+ } ) ;
1202+ document . getElementById ( 'pp-force' ) ?. addEventListener ( 'change' , e => {
1203+ State . pipeline . force = e . target . checked ;
1204+ } ) ;
1205+ for ( const [ k , id ] of [
1206+ [ 'mat' , 'pp-step-mat' ] , [ 'vid' , 'pp-step-vid' ] , [ 'trans' , 'pp-step-trans' ] ,
1207+ [ 'align' , 'pp-step-align' ] , [ 'gen' , 'pp-step-gen' ] ,
1208+ ] ) {
1209+ document . getElementById ( id ) ?. addEventListener ( 'change' , e => {
1210+ State . pipeline . steps [ k ] = e . target . checked ;
1211+ } ) ;
1212+ }
1213+
1214+ // Auto-fill course name if blank on first render
1215+ const ppCourseEl = document . getElementById ( 'pp-course' ) ;
1216+ const ppNameEl = document . getElementById ( 'pp-course-name' ) ;
1217+ if ( ppCourseEl && ppNameEl && ! ppNameEl . value . trim ( ) && ppCourseEl . value ) {
1218+ ppNameEl . value = courseNameFromId ( ppCourseEl . value ) ;
1219+ State . pipeline . courseName = ppNameEl . value ;
1220+ }
11311221
11321222 document . getElementById ( 'pp-run-btn' ) ?. addEventListener ( 'click' , async ( ) => {
11331223 const cid = document . getElementById ( 'pp-course' ) ?. value ;
0 commit comments