spectoflow 0.17.0 → 0.17.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/package.json +1 -1
- package/templates/README.md +4 -0
- package/templates/agents/qa-engineer.md +8 -5
- package/templates/dashboard/public/app.js +160 -124
- package/templates/dashboard/public/designs/console.css +15 -1
- package/templates/dashboard/public/designs/console.js +19 -0
- package/templates/dashboard/public/designs/orbit.css +42 -18
- package/templates/dashboard/public/designs/orbit.js +30 -5
- package/templates/dashboard/public/i18n.js +577 -0
- package/templates/dashboard/public/index.html +103 -86
- package/templates/dashboard/public/styles.css +15 -1
- package/templates/dashboard/server.js +1 -0
- package/templates/skills/write-e2e-tests/SKILL.md +72 -17
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
// Kept as a mutable object (not reassigned) so every existing STATUS[key] lookup keeps working
|
|
3
|
+
// unchanged; updateStatusLabels() (called at the top of every render()) refreshes its values from
|
|
4
|
+
// the current UI language, so a language change re-labels every status chip/column/legend for free.
|
|
2
5
|
const STATUS = { todo:'To do', in_progress:'In progress', to_validate:'To validate', to_analyze:'To analyze', done:'Done', blocked:'Blocked' };
|
|
6
|
+
function updateStatusLabels(){ for(const k of Object.keys(STATUS)) STATUS[k]=t('status.'+k); }
|
|
3
7
|
let P = null, openTaskId = null;
|
|
4
8
|
let filter = { status: 'all', q: '' }; // board filter state — client-side only, read-only
|
|
5
9
|
let boardView = (()=>{ try{ return localStorage.getItem('spf-board-view')||'list'; }catch{ return 'list'; } })(); // 'list' | 'kanban'
|
|
@@ -73,8 +77,8 @@ function renderApproval(container){
|
|
|
73
77
|
if(!o || o.status!=='awaiting_approval') return;
|
|
74
78
|
row=el('div','approval');
|
|
75
79
|
row.append(el('div','msg-role','orchestrator · awaiting approval'));
|
|
76
|
-
const a=el('button','btn primary','
|
|
77
|
-
const c=el('button','btn','
|
|
80
|
+
const a=el('button','btn primary',t('action.approve')); a.addEventListener('click',()=>approve('approve'));
|
|
81
|
+
const c=el('button','btn',t('action.cancel')); c.addEventListener('click',()=>approve('cancel'));
|
|
78
82
|
const acts=el('div','c-actions'); acts.append(a,c); row.append(acts);
|
|
79
83
|
container.append(row); scrollChat(container);
|
|
80
84
|
}
|
|
@@ -117,9 +121,11 @@ function flash(){ const s=$('#sync'); s.classList.add('saving'); $('#syncLabel')
|
|
|
117
121
|
|
|
118
122
|
function render(){
|
|
119
123
|
const c = P.config||{};
|
|
120
|
-
|
|
124
|
+
i18nSetLang(c.language||'en'); updateStatusLabels(); // language drives the whole UI, not just agent output
|
|
125
|
+
$('#projectName').textContent = P.projectName || c.projectType || 'project';
|
|
121
126
|
const bv=$('#brandVer'); if(bv){ if(P.version){ bv.textContent='v'+P.version; bv.hidden=false; } else { bv.hidden=true; } }
|
|
122
|
-
|
|
127
|
+
// mode/language are edited live from the bar itself — see the #topMode/#topLang selects synced
|
|
128
|
+
// in renderSettings() below (called on every render tick) and saved via saveSettings().
|
|
123
129
|
// agent select — widget (#runAgent) and Chat tab (#tabRunAgent) each get their own populated
|
|
124
130
|
// <select>, since an id can't be shared by two elements; same option list, same source of truth.
|
|
125
131
|
const runners=Object.keys((c.runners)||{claude:1});
|
|
@@ -131,11 +137,12 @@ function render(){
|
|
|
131
137
|
const meterFill=$('#globalMeterFill');
|
|
132
138
|
if(meterFill) meterFill.style.width=(s.pct||0)+'%';
|
|
133
139
|
const meter=$('#globalMeter');
|
|
134
|
-
if(meter) meter.title
|
|
140
|
+
if(meter) meter.title=`${t('kpi.globalProgress')}: ${s.pct}% (${s.done}/${s.total} ${t('kpi.tasksLabel')})`;
|
|
135
141
|
renderOverview(); renderBoard(); renderBacklog(); renderWorkflow(); renderTeam();
|
|
136
142
|
renderChatLog($('#chatLog')); renderChatLog($('#chatTabLog'));
|
|
137
143
|
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderSettings();
|
|
138
144
|
applyActiveTab(); // re-apply the current tab so an SSE-driven re-render never resets to Board
|
|
145
|
+
applyI18nStatic(); // re-translate the static markup (nav, headers, placeholders…) for this tick's language
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
// ---- Requests tab: tasks awaiting review/input (to_validate / to_analyze) ----
|
|
@@ -144,7 +151,7 @@ function renderRequests(){
|
|
|
144
151
|
const toAsk=SpectoStats.stats(P).toAsk||[];
|
|
145
152
|
const count=$('#requestsCount'); if(count) count.textContent=toAsk.length;
|
|
146
153
|
list.innerHTML='';
|
|
147
|
-
if(!toAsk.length){ list.append(li('empty','
|
|
154
|
+
if(!toAsk.length){ list.append(li('empty',t('requests.empty'))); return; }
|
|
148
155
|
toAsk.forEach(t=>{
|
|
149
156
|
const row=el('li','request-row'); row.tabIndex=0;
|
|
150
157
|
row.append(el('span','request-id',t.id));
|
|
@@ -159,20 +166,35 @@ function renderRequests(){
|
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
// ---- right sidebar: "Journal" (read-only runtime.messages feed) ----
|
|
169
|
+
// Capped to the 5 most recent entries by default — a long-running project's journal used to render
|
|
170
|
+
// its entire history inline and blow up the sidebar. "See more" reveals the rest for this session
|
|
171
|
+
// (client-side only; not persisted, so the rail opens compact again next visit).
|
|
172
|
+
const JOURNAL_PAGE=5;
|
|
173
|
+
let journalExpanded=false;
|
|
162
174
|
function renderSidebar(){ renderJournal(); }
|
|
163
175
|
function renderJournal(){
|
|
164
176
|
const box=$('#journal'); if(!box) return;
|
|
165
177
|
const msgs=((P.runtime&&P.runtime.messages)||[]).slice().reverse(); // reverse-chronological
|
|
166
178
|
$('#journalCount').textContent=msgs.length;
|
|
167
179
|
box.innerHTML='';
|
|
168
|
-
if(!msgs.length){ box.append(el('div','empty','
|
|
169
|
-
msgs.
|
|
180
|
+
if(!msgs.length){ box.append(el('div','empty',t('journal.empty'))); journalToggleBtn(0,0); return; }
|
|
181
|
+
const shown = journalExpanded ? msgs : msgs.slice(0,JOURNAL_PAGE);
|
|
182
|
+
shown.forEach(m=>{
|
|
170
183
|
const row=el('div','journal-row'+(m.role==='user'?' j-you':' k-'+(m.kind||'message')));
|
|
171
184
|
row.append(el('div','journal-head', m.role + (m.agent&&m.agent!==m.role?(' · '+m.agent):'')));
|
|
172
185
|
row.append(el('div','journal-text', m.text));
|
|
173
186
|
box.append(row);
|
|
174
187
|
});
|
|
188
|
+
journalToggleBtn(msgs.length, shown.length);
|
|
175
189
|
}
|
|
190
|
+
function journalToggleBtn(total,shownCount){
|
|
191
|
+
const btn=$('#journalMore'); if(!btn) return;
|
|
192
|
+
if(total<=JOURNAL_PAGE){ btn.hidden=true; return; }
|
|
193
|
+
btn.hidden=false;
|
|
194
|
+
btn.textContent = journalExpanded ? t('journal.seeLess') : t('journal.seeMore')+` (${total-shownCount})`;
|
|
195
|
+
}
|
|
196
|
+
const journalMoreBtn=$('#journalMore');
|
|
197
|
+
if(journalMoreBtn) journalMoreBtn.addEventListener('click',()=>{ journalExpanded=!journalExpanded; renderJournal(); });
|
|
176
198
|
|
|
177
199
|
// ---- SVG helpers ----------------------------------------------------------
|
|
178
200
|
// The chart *markup* (donut/ring/bars arcs & rows) is built by the pure,
|
|
@@ -230,20 +252,20 @@ function renderOverview(){
|
|
|
230
252
|
|
|
231
253
|
// KPI row
|
|
232
254
|
const kpis=el('div','kpi-row');
|
|
233
|
-
kpis.append(kpiCard('
|
|
234
|
-
kpis.append(kpiCard('
|
|
235
|
-
kpis.append(kpiCard('
|
|
255
|
+
kpis.append(kpiCard(t('kpi.globalProgress'), ring(s.pct,72), `${s.done}/${s.total} ${t('kpi.tasksLabel')}`, cssv('--s-done')));
|
|
256
|
+
kpis.append(kpiCard(t('status.in_progress'), numBlock(s.byStatus.in_progress||0,'var(--s-in_progress)'), t('kpi.tasksLabel'), cssv('--s-in_progress')));
|
|
257
|
+
kpis.append(kpiCard(t('status.to_validate'), numBlock(s.byStatus.to_validate||0,'var(--s-to_validate)'), t('kpi.awaitingReview'), cssv('--s-to_validate')));
|
|
236
258
|
const r=s.running||{};
|
|
237
|
-
let runVal='—', runSub='
|
|
238
|
-
if(r.agents>0){ runVal=`${r.agents} running`; runSub='
|
|
239
|
-
else if(r.orchestration&&r.orchestration.status){ runVal=r.orchestration.status; runSub='orchestration'; }
|
|
240
|
-
else if(r.lastRun){ runVal=r.lastRun.status||'—'; runSub='
|
|
241
|
-
kpis.append(kpiCard('
|
|
259
|
+
let runVal='—', runSub=t('kpi.noRunsYet');
|
|
260
|
+
if(r.agents>0){ runVal=`${r.agents} ${t('kpi.running').toLowerCase()}`; runSub=t('kpi.agentsActive'); }
|
|
261
|
+
else if(r.orchestration&&r.orchestration.status){ runVal=r.orchestration.status; runSub=t('kpi.orchestration'); }
|
|
262
|
+
else if(r.lastRun){ runVal=r.lastRun.status||'—'; runSub=t('kpi.lastPrefix')+(r.lastRun.tool||'—'); }
|
|
263
|
+
kpis.append(kpiCard(t('kpi.running'), numBlock(runVal, r.agents>0?'var(--signal)':'var(--muted)', true), runSub, cssv('--signal')));
|
|
242
264
|
box.append(kpis);
|
|
243
265
|
|
|
244
266
|
// Status donut + legend
|
|
245
267
|
const segments=s.statuses.map(k=>({key:k,value:s.byStatus[k]||0,color:cssv('--s-'+k)}));
|
|
246
|
-
const d=donut(segments,140,{center:String(s.total),sub:'
|
|
268
|
+
const d=donut(segments,140,{center:String(s.total),sub:t('chart.tasksSub')});
|
|
247
269
|
const legend=el('div','legend');
|
|
248
270
|
segments.forEach(seg=>{
|
|
249
271
|
const item=el('div','legend-item');
|
|
@@ -260,19 +282,19 @@ function renderOverview(){
|
|
|
260
282
|
if(hist.length===1){ hist=[{date:hist[0].date,total:0,done:0},hist[0]]; }
|
|
261
283
|
const area=htmlBlock('area-wrap', hist.length
|
|
262
284
|
? SpectoCharts.area(
|
|
263
|
-
[{name:'
|
|
264
|
-
{name:'
|
|
285
|
+
[{name:t('chart.scope'),color:cssv('--cool'),data:hist.map(h=>h.total)},
|
|
286
|
+
{name:t('chart.delivered'),color:cssv('--signal'),data:hist.map(h=>h.done)}],
|
|
265
287
|
hist.map(h=>(h.date||'').slice(5)))
|
|
266
|
-
: '<div class="empty">
|
|
288
|
+
: '<div class="empty">'+t('chart.noHistory')+'</div>');
|
|
267
289
|
// enrich the area's hit-rect tooltips with the actual scope/delivered values
|
|
268
290
|
// for that point (charts.js keeps them pure — just the date label)
|
|
269
291
|
area.querySelectorAll('.area-hit').forEach((hit,i)=>{
|
|
270
292
|
const h=hist[i]; if(!h) return;
|
|
271
|
-
hit.dataset.tip = `<b>${h.date||''}</b><br
|
|
293
|
+
hit.dataset.tip = `<b>${h.date||''}</b><br>${t('chart.scope')}: ${h.total||0} · ${t('chart.delivered')}: ${h.done||0}`;
|
|
272
294
|
});
|
|
273
295
|
const topRow=el('div','overview-top');
|
|
274
|
-
topRow.append(ocard('
|
|
275
|
-
topRow.append(ocard('
|
|
296
|
+
topRow.append(ocard(t('chart.statusDistribution'), donutRow));
|
|
297
|
+
topRow.append(ocard(t('chart.scopeVsDelivered'), area));
|
|
276
298
|
box.append(topRow);
|
|
277
299
|
|
|
278
300
|
// Workflow-at-a-glance strip (reuses the wf-arrow flow animation)
|
|
@@ -284,8 +306,8 @@ function renderOverview(){
|
|
|
284
306
|
strip.append(node);
|
|
285
307
|
if(i<steps.length-1){ const a=el('div','wf-arrow'+(st.enabled&&steps[i+1].enabled?'':' off')); strip.append(a); }
|
|
286
308
|
});
|
|
287
|
-
if(!steps.length) strip.append(el('div','empty','
|
|
288
|
-
box.append(ocard('
|
|
309
|
+
if(!steps.length) strip.append(el('div','empty',t('board.noWorkflow')));
|
|
310
|
+
box.append(ocard(t('board.workflowGlance'), strip));
|
|
289
311
|
|
|
290
312
|
// Per-phase progress bars — only phases that actually hold tasks (headings with no checkbox tasks
|
|
291
313
|
// are noise, not phases), and cap the list height with an internal scroll so a big project with
|
|
@@ -294,7 +316,7 @@ function renderOverview(){
|
|
|
294
316
|
if(phaseRows.length){
|
|
295
317
|
const barsEl=bars(phaseRows);
|
|
296
318
|
if(phaseRows.length>8) barsEl.classList.add('scroll-cap');
|
|
297
|
-
box.append(ocard(
|
|
319
|
+
box.append(ocard(t('board.phaseProgress',{n:phaseRows.length}), barsEl));
|
|
298
320
|
}
|
|
299
321
|
}
|
|
300
322
|
|
|
@@ -313,12 +335,12 @@ function renderBoard(){
|
|
|
313
335
|
updateFilterChips();
|
|
314
336
|
|
|
315
337
|
const specs=$('#specs'); specs.innerHTML=''; $('#specsCount').textContent=(P.specs||[]).length;
|
|
316
|
-
if(!(P.specs||[]).length) specs.append(li('empty','
|
|
338
|
+
if(!(P.specs||[]).length) specs.append(li('empty',t('board.noneYet')));
|
|
317
339
|
(P.specs||[]).forEach(s=> specs.append(li(null,s)));
|
|
318
340
|
|
|
319
341
|
const running = (P.runtime&&P.runtime.agents||[]).filter(a=>a.status==='running');
|
|
320
342
|
const rl=$('#running'); rl.innerHTML=''; $('#runCount').textContent=running.length;
|
|
321
|
-
if(!running.length) rl.append(li('empty','
|
|
343
|
+
if(!running.length) rl.append(li('empty',t('board.noAgentRunning')));
|
|
322
344
|
running.forEach(a=>{ const e=li('run-live',''); e.innerHTML=`<b>${a.tool}</b> · ${a.task||'—'}`; rl.append(e); });
|
|
323
345
|
|
|
324
346
|
const board=$('#board'); board.innerHTML='';
|
|
@@ -357,8 +379,8 @@ function renderKanban(board, tasks){
|
|
|
357
379
|
}
|
|
358
380
|
function updateBoardViewToggle(){ $$('#boardViewToggle .vt-btn').forEach(b=> b.classList.toggle('active', b.dataset.view===boardView)); }
|
|
359
381
|
function li(cls,txt){ const e=el('li',cls); e.textContent=txt; return e; }
|
|
360
|
-
function emptyState(){ const d=el('div','empty'); d.style.padding='40px'; d.textContent='
|
|
361
|
-
function noMatchState(){ const d=el('div','empty'); d.style.padding='40px'; d.textContent='
|
|
382
|
+
function emptyState(){ const d=el('div','empty'); d.style.padding='40px'; d.textContent=t('board.noPlans'); return d; }
|
|
383
|
+
function noMatchState(){ const d=el('div','empty'); d.style.padding='40px'; d.textContent=t('board.noTasksMatch'); return d; }
|
|
362
384
|
|
|
363
385
|
// ---- Backlog tab: flat, sortable, filterable table of every task ----------
|
|
364
386
|
// Rows are built from P.plans (not allTasks()) so each task carries its phase title.
|
|
@@ -408,21 +430,21 @@ function renderBacklog(){
|
|
|
408
430
|
if(backlogPage>pages) backlogPage=pages;
|
|
409
431
|
if(backlogPage<1) backlogPage=1;
|
|
410
432
|
body.innerHTML='';
|
|
411
|
-
if(!all.length){ body.append(backlogEmptyRow('
|
|
412
|
-
if(!filtered.length){ body.append(backlogEmptyRow('
|
|
433
|
+
if(!all.length){ body.append(backlogEmptyRow(t('board.noPlans'))); return renderBacklogPager(0,1); }
|
|
434
|
+
if(!filtered.length){ body.append(backlogEmptyRow(t('board.noTasksMatch'))); return renderBacklogPager(0,1); }
|
|
413
435
|
const start=(backlogPage-1)*BACKLOG_PAGE;
|
|
414
436
|
filtered.slice(start,start+BACKLOG_PAGE).forEach(r=> body.append(backlogRow(r)));
|
|
415
437
|
renderBacklogPager(filtered.length,pages);
|
|
416
438
|
}
|
|
417
439
|
function renderBacklogPager(total,pages){
|
|
418
440
|
const pager=$('#backlogPager'); if(!pager) return; pager.innerHTML='';
|
|
419
|
-
if(total<=BACKLOG_PAGE){ if(total) pager.append(el('span','pager-info'
|
|
420
|
-
const prev=el('button','pager-btn','
|
|
441
|
+
if(total<=BACKLOG_PAGE){ if(total) pager.append(el('span','pager-info', t(total>1?'backlog.pagerTasks':'backlog.pagerTask',{n:total}))); return; }
|
|
442
|
+
const prev=el('button','pager-btn',t('backlog.prev')); prev.disabled=backlogPage<=1;
|
|
421
443
|
prev.addEventListener('click',()=>{ backlogPage--; renderBacklog(); });
|
|
422
|
-
const next=el('button','pager-btn','
|
|
444
|
+
const next=el('button','pager-btn',t('backlog.next')); next.disabled=backlogPage>=pages;
|
|
423
445
|
next.addEventListener('click',()=>{ backlogPage++; renderBacklog(); });
|
|
424
446
|
const from=(backlogPage-1)*BACKLOG_PAGE+1, to=Math.min(total,backlogPage*BACKLOG_PAGE);
|
|
425
|
-
pager.append(prev, el('span','pager-info'
|
|
447
|
+
pager.append(prev, el('span','pager-info',t('backlog.pagerRange',{from,to,total,page:backlogPage,pages})), next);
|
|
426
448
|
}
|
|
427
449
|
function backlogEmptyRow(txt){
|
|
428
450
|
const tr=el('tr','backlog-empty-row'); const td=el('td',null,txt); td.colSpan=7; tr.append(td); return tr;
|
|
@@ -451,12 +473,12 @@ function loadExpanded(){
|
|
|
451
473
|
}
|
|
452
474
|
function saveExpanded(set){ try{ localStorage.setItem('spf-expanded', JSON.stringify([...set])); }catch{} }
|
|
453
475
|
let expandedPhases=loadExpanded();
|
|
454
|
-
function allPhaseTitles(){ const
|
|
476
|
+
function allPhaseTitles(){ const set=new Set(); (P.plans||[]).forEach(pl=> pl.phases.forEach(ph=> set.add(ph.title))); return [...set]; }
|
|
455
477
|
function updatePhaseToggleAll(){
|
|
456
478
|
const btn=$('#phaseToggleAll'); if(!btn) return;
|
|
457
479
|
const titles=allPhaseTitles();
|
|
458
|
-
const allOpen = titles.length>0 && titles.every(
|
|
459
|
-
btn.textContent = allOpen ? '
|
|
480
|
+
const allOpen = titles.length>0 && titles.every(x=> expandedPhases.has(x));
|
|
481
|
+
btn.textContent = allOpen ? t('board.collapseAll') : t('board.expandAll');
|
|
460
482
|
btn.dataset.state = allOpen ? 'open' : 'closed';
|
|
461
483
|
}
|
|
462
484
|
|
|
@@ -515,17 +537,17 @@ function renderTask(t){
|
|
|
515
537
|
// detail zone teaches the user what the pipeline is, not just that a step exists.
|
|
516
538
|
function wfDesc(s){
|
|
517
539
|
const n=String(s.name||'').toLowerCase();
|
|
518
|
-
if(/brainstorm|idea|intake/.test(n)) return '
|
|
519
|
-
if(/analy/.test(n)) return '
|
|
520
|
-
if(/spec/.test(n)) return '
|
|
521
|
-
if(/plan/.test(n)) return '
|
|
522
|
-
if(/develop|implement|\bcode\b/.test(n)) return '
|
|
523
|
-
if(/integration/.test(n)) return '
|
|
524
|
-
if(/end.?to.?end|e2e/.test(n)) return '
|
|
525
|
-
if(/unit/.test(n)||/\btest/.test(n)) return '
|
|
526
|
-
if(/review/.test(n)) return '
|
|
527
|
-
if(/deploy|ship|release|publish/.test(n)) return '
|
|
528
|
-
return '
|
|
540
|
+
if(/brainstorm|idea|intake/.test(n)) return t('wf.desc.brainstorm');
|
|
541
|
+
if(/analy/.test(n)) return t('wf.desc.analysis');
|
|
542
|
+
if(/spec/.test(n)) return t('wf.desc.spec');
|
|
543
|
+
if(/plan/.test(n)) return t('wf.desc.plan');
|
|
544
|
+
if(/develop|implement|\bcode\b/.test(n)) return t('wf.desc.develop');
|
|
545
|
+
if(/integration/.test(n)) return t('wf.desc.integration');
|
|
546
|
+
if(/end.?to.?end|e2e/.test(n)) return t('wf.desc.e2e');
|
|
547
|
+
if(/unit/.test(n)||/\btest/.test(n)) return t('wf.desc.unit');
|
|
548
|
+
if(/review/.test(n)) return t('wf.desc.review');
|
|
549
|
+
if(/deploy|ship|release|publish/.test(n)) return t('wf.desc.deploy');
|
|
550
|
+
return t('wf.desc.fallback');
|
|
529
551
|
}
|
|
530
552
|
let wfPopStep=null; // name of the step whose click-popover is open (null = closed)
|
|
531
553
|
|
|
@@ -536,11 +558,11 @@ let wfPopStep=null; // name of the step whose click-popover is open (null = clos
|
|
|
536
558
|
function renderWorkflow(){
|
|
537
559
|
const box=$('#wfDiagram'); box.innerHTML='';
|
|
538
560
|
const steps=P.workflow||[];
|
|
539
|
-
if(!steps.length){ box.append(el('div','empty','
|
|
561
|
+
if(!steps.length){ box.append(el('div','empty',t('board.noWorkflow'))); closeWfPop(); return; }
|
|
540
562
|
const enabledCount=steps.filter(s=>s.enabled).length;
|
|
541
563
|
const legend=el('div','wf-legend');
|
|
542
564
|
legend.append(el('span','wf-legend-dot'));
|
|
543
|
-
legend.append(el('span','wf-legend-txt'
|
|
565
|
+
legend.append(el('span','wf-legend-txt',t('workflow.stepsEnabled',{enabled:enabledCount,total:steps.length})));
|
|
544
566
|
box.append(legend);
|
|
545
567
|
const pipe=el('div','wf-pipeline');
|
|
546
568
|
steps.forEach((s,i)=>{
|
|
@@ -551,7 +573,7 @@ function renderWorkflow(){
|
|
|
551
573
|
circle.innerHTML=(typeof ICON!=='undefined'&&ICON.wf)?ICON.wf(s.name):'';
|
|
552
574
|
step.append(circle);
|
|
553
575
|
const cap=el('div','wf-caption'); cap.append(document.createTextNode(s.name));
|
|
554
|
-
if(s.optional) cap.append(el('span','wf-opt2','optional'));
|
|
576
|
+
if(s.optional) cap.append(el('span','wf-opt2',t('workflow.optional')));
|
|
555
577
|
step.append(cap);
|
|
556
578
|
const sel=(e)=>{ if(e) e.stopPropagation(); if(wfPopStep===s.name) closeWfPop(); else openWfPop(s.name); };
|
|
557
579
|
step.addEventListener('click',sel);
|
|
@@ -570,19 +592,19 @@ function wfPopFill(pop, s, idx){
|
|
|
570
592
|
const head=el('div','wf-detail-head');
|
|
571
593
|
head.append(el('span','wf-detail-num',String(idx+1)));
|
|
572
594
|
head.append(el('span','wf-detail-name',s.name));
|
|
573
|
-
head.append(el('span','wf-detail-status '+(s.enabled?'on':'off'), s.enabled?'enabled':'disabled'));
|
|
574
|
-
if(s.optional) head.append(el('span','wf-opt','optional'));
|
|
595
|
+
head.append(el('span','wf-detail-status '+(s.enabled?'on':'off'), s.enabled?t('workflow.enabled'):t('workflow.disabled')));
|
|
596
|
+
if(s.optional) head.append(el('span','wf-opt',t('workflow.optional')));
|
|
575
597
|
pop.append(head);
|
|
576
598
|
pop.append(el('p','wf-detail-desc', wfDesc(s)));
|
|
577
599
|
const grid=el('div','wf-detail-grid');
|
|
578
|
-
grid.append(wfDetailRow('
|
|
579
|
-
grid.append(wfDetailRow('
|
|
580
|
-
grid.append(wfDetailRow('
|
|
581
|
-
if(skill&&skill.standard) grid.append(wfDetailRow('
|
|
582
|
-
if(skill&&(skill.inputs||skill.outputs)) grid.append(wfDetailRow('
|
|
600
|
+
grid.append(wfDetailRow(t('workflow.capability'), s.cap||'—'));
|
|
601
|
+
grid.append(wfDetailRow(t('workflow.handledBy'), agent?(agent.title||agent.name):'—'));
|
|
602
|
+
grid.append(wfDetailRow(t('workflow.skill'), s.skill||'—'));
|
|
603
|
+
if(skill&&skill.standard) grid.append(wfDetailRow(t('workflow.standard'), skill.standard));
|
|
604
|
+
if(skill&&(skill.inputs||skill.outputs)) grid.append(wfDetailRow(t('workflow.flow'), (skill.inputs||'—')+' → '+(skill.outputs||'—')));
|
|
583
605
|
pop.append(grid);
|
|
584
|
-
if(skill&&skill.description){ const sk=el('div','wf-detail-skill'); sk.append(el('b',null,'
|
|
585
|
-
const btn=el('button','btn '+(s.enabled?'':'primary')+' wf-detail-btn', s.enabled?'
|
|
606
|
+
if(skill&&skill.description){ const sk=el('div','wf-detail-skill'); sk.append(el('b',null,t('workflow.skillPrefix',{name:skill.name}))); sk.append(document.createTextNode(skill.description)); pop.append(sk); }
|
|
607
|
+
const btn=el('button','btn '+(s.enabled?'':'primary')+' wf-detail-btn', s.enabled?t('workflow.disableStep'):t('workflow.enableStep'));
|
|
586
608
|
btn.addEventListener('click',(e)=>{ e.stopPropagation(); toggleStep(s.name); });
|
|
587
609
|
const actions=el('div','wf-pop-actions'); actions.append(btn); pop.append(actions);
|
|
588
610
|
}
|
|
@@ -629,27 +651,27 @@ function renderAttention(){
|
|
|
629
651
|
$$('.attn-filters .fchip').forEach(b=> b.classList.toggle('active', b.dataset.attn===attnFilter));
|
|
630
652
|
const shown=items.filter(i=> attnFilter==='all' ? true : attnFilter==='resolved' ? i.status==='resolved' : i.status!=='resolved');
|
|
631
653
|
list.innerHTML='';
|
|
632
|
-
if(!shown.length){ list.append(el('div','empty', attnFilter==='resolved'?'
|
|
654
|
+
if(!shown.length){ list.append(el('div','empty', attnFilter==='resolved'?t('attn.emptyResolved'):t('attn.emptyOpen'))); return; }
|
|
633
655
|
shown.forEach(it=> list.append(attnRow(it)));
|
|
634
656
|
}
|
|
635
657
|
function attnRow(it){
|
|
636
658
|
const row=el('div','attn-row'+(it.status==='resolved'?' is-resolved':'')+(it.source==='agent'?' from-agent':''));
|
|
637
659
|
const head=el('div','attn-head');
|
|
638
|
-
head.append(el('span','attn-src '+(it.source==='agent'?'is-agent':'is-user'), it.source==='agent'?('⚑ '+(it.by||'
|
|
660
|
+
head.append(el('span','attn-src '+(it.source==='agent'?'is-agent':'is-user'), it.source==='agent'?('⚑ '+(it.by||t('attn.agentFallback'))):('✎ '+t('attn.you'))));
|
|
639
661
|
if(it.at) head.append(el('span','attn-time',(String(it.at).replace('T',' ')).slice(0,16)));
|
|
640
|
-
if(it.status==='resolved') head.append(el('span','chip s-done', it.promotedTo?('→ '+it.promotedTo):'
|
|
662
|
+
if(it.status==='resolved') head.append(el('span','chip s-done', it.promotedTo?('→ '+it.promotedTo):t('attn.resolvedChip')));
|
|
641
663
|
row.append(head);
|
|
642
664
|
const txt=el('div','attn-text', it.text); row.append(txt);
|
|
643
665
|
const acts=el('div','attn-actions');
|
|
644
666
|
if(it.status!=='resolved'){
|
|
645
|
-
const val=el('button','btn primary','
|
|
646
|
-
const res=el('button','btn','
|
|
647
|
-
const edit=el('button','btn','
|
|
667
|
+
const val=el('button','btn primary',t('action.validateToTask')); val.addEventListener('click',()=>promoteAttn(it.id));
|
|
668
|
+
const res=el('button','btn',t('action.resolve')); res.addEventListener('click',()=>patchAttn(it.id,{status:'resolved'}));
|
|
669
|
+
const edit=el('button','btn',t('action.edit')); edit.addEventListener('click',()=>editAttn(it,txt));
|
|
648
670
|
acts.append(val,res,edit);
|
|
649
671
|
}else{
|
|
650
|
-
const re=el('button','btn','
|
|
672
|
+
const re=el('button','btn',t('action.reopen')); re.addEventListener('click',()=>patchAttn(it.id,{status:'open'})); acts.append(re);
|
|
651
673
|
}
|
|
652
|
-
const del=el('button','btn danger','
|
|
674
|
+
const del=el('button','btn danger',t('action.delete')); del.addEventListener('click',()=>deleteAttn(it.id));
|
|
653
675
|
acts.append(del); row.append(acts);
|
|
654
676
|
return row;
|
|
655
677
|
}
|
|
@@ -665,12 +687,19 @@ async function patchAttn(id,patch){ flash(); await fetch('/api/attention/'+encod
|
|
|
665
687
|
async function deleteAttn(id){ flash(); await fetch('/api/attention/'+encodeURIComponent(id),{method:'DELETE'}); }
|
|
666
688
|
async function promoteAttn(id){ flash(); await fetch('/api/attention/'+encodeURIComponent(id)+'/promote',{method:'POST'}); }
|
|
667
689
|
|
|
668
|
-
// ---- Settings tab: change autonomy mode + output language (writes config.json) ----
|
|
669
|
-
|
|
670
|
-
|
|
690
|
+
// ---- Settings tab + topbar quick-switch: change autonomy mode + output language (writes config.json) ----
|
|
691
|
+
// Mode/language can be changed from two places — the Settings tab (#setMode/#setLang) and the
|
|
692
|
+
// always-visible topbar selects (#topMode/#topLang, in .brand-sub, present in every design). Both
|
|
693
|
+
// pairs are kept in sync: renderSettings() (every render tick) writes the current config into all
|
|
694
|
+
// four; a change on any one immediately mirrors into its sibling before saving, so saveSettings()
|
|
695
|
+
// never reads a stale value from the pair the user didn't touch.
|
|
696
|
+
function fillLangSelect(sel,lang){
|
|
697
|
+
if(!sel) return;
|
|
671
698
|
if(![...sel.options].some(o=>o.value===lang)){ const o=document.createElement('option'); o.value=lang; o.textContent=lang; sel.append(o); }
|
|
672
699
|
sel.value=lang;
|
|
673
700
|
}
|
|
701
|
+
function setLangSelect(lang){ fillLangSelect($('#setLang'),lang); fillLangSelect($('#topLang'),lang); }
|
|
702
|
+
function setModeSelects(mode){ [$('#setMode'),$('#topMode')].forEach(s=>{ if(s) s.value=mode; }); }
|
|
674
703
|
// ---- design skins (data-design) — switchable, persisted per viewer + as the project default ----
|
|
675
704
|
function currentDesign(){ return document.documentElement.getAttribute('data-design')||'console'; }
|
|
676
705
|
function applyDesign(id){ document.documentElement.setAttribute('data-design',id); try{ localStorage.setItem('spf-design',id); }catch{} }
|
|
@@ -678,7 +707,7 @@ async function saveDesign(id){ applyDesign(id); if(P) render(); /* re-read token
|
|
|
678
707
|
|
|
679
708
|
function renderSettings(){
|
|
680
709
|
const c=(P&&P.config)||{};
|
|
681
|
-
|
|
710
|
+
setModeSelects(c.mode||'semi');
|
|
682
711
|
setLangSelect(c.language||'en');
|
|
683
712
|
// design switcher — options from the DESIGNS registry (designs.js)
|
|
684
713
|
const dsel=$('#setDesign');
|
|
@@ -694,15 +723,16 @@ function renderSettings(){
|
|
|
694
723
|
const box=$('#settingsReadonly');
|
|
695
724
|
if(box){
|
|
696
725
|
box.innerHTML='';
|
|
697
|
-
const rows=[['
|
|
698
|
-
['
|
|
699
|
-
['
|
|
726
|
+
const rows=[[t('info.activeAgent'),c.agent||'—'],[t('info.projectType'),c.projectType||'—'],
|
|
727
|
+
[t('field.plansFolder'),c.plansDir||'plans'],[t('field.specsFolder'),c.specsDir||'specs'],
|
|
728
|
+
[t('field.frameworkVersion'), P&&P.version?('v'+P.version):'—']];
|
|
700
729
|
rows.forEach(([k,v])=>{ const r=el('div','settings-ro-row'); r.append(el('span','settings-ro-k',k), el('span','settings-ro-v',String(v))); box.append(r); });
|
|
701
730
|
}
|
|
702
731
|
const fv=$('#footerVer'); if(fv) fv.textContent = (P&&P.version) ? ('v'+P.version) : '';
|
|
703
732
|
}
|
|
704
733
|
async function saveSettings(){
|
|
705
|
-
flash();
|
|
734
|
+
flash();
|
|
735
|
+
const mode=($('#setMode')||$('#topMode')).value, language=($('#setLang')||$('#topLang')).value;
|
|
706
736
|
await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode,language})});
|
|
707
737
|
const s=$('#settingsSaved'); if(s){ s.hidden=false; setTimeout(()=>{ s.hidden=true; },1500); }
|
|
708
738
|
}
|
|
@@ -731,8 +761,8 @@ function agentCard(a){
|
|
|
731
761
|
const c=el('div','card'); c.tabIndex=0;
|
|
732
762
|
c.append(el('div','ct',a.title||a.name));
|
|
733
763
|
if(a.capability) c.append(el('div','cc',a.capability));
|
|
734
|
-
const std=chipRow('
|
|
735
|
-
const uses=chipRow('
|
|
764
|
+
const std=chipRow(t('team.standardsLabel'),a.standards); if(std) c.append(std);
|
|
765
|
+
const uses=chipRow(t('team.usesLabel'),a.uses); if(uses) c.append(uses);
|
|
736
766
|
if(a.description) c.append(el('div','cd',a.description));
|
|
737
767
|
const open=()=>openFileDrawer('agent',a);
|
|
738
768
|
c.addEventListener('click',open);
|
|
@@ -743,7 +773,7 @@ function skillCard(s){
|
|
|
743
773
|
const c=el('div','card'); c.tabIndex=0;
|
|
744
774
|
c.append(el('div','ct',s.name));
|
|
745
775
|
if(s.capability) c.append(el('div','cc',s.capability));
|
|
746
|
-
const std=chipRow('
|
|
776
|
+
const std=chipRow(t('team.standardLabel'), s.standard?[s.standard]:null); if(std) c.append(std);
|
|
747
777
|
if(s.inputs||s.outputs) c.append(el('div','io', (s.inputs||'—')+' → '+(s.outputs||'—')));
|
|
748
778
|
if(s.description) c.append(el('div','cd',s.description));
|
|
749
779
|
const open=()=>openFileDrawer('skill',s);
|
|
@@ -803,19 +833,19 @@ async function openFileDrawer(kind,obj){
|
|
|
803
833
|
openTaskId=null;
|
|
804
834
|
const rel = kind==='agent' ? ('agents/'+(obj.file||(obj.name+'.md'))) : ('skills/'+obj.name+'/SKILL.md');
|
|
805
835
|
const b=$('#drawerBody'); b.innerHTML='';
|
|
806
|
-
b.append(el('div','d-id', kind==='agent'?'
|
|
836
|
+
b.append(el('div','d-id', kind==='agent'?t('drawer.agent'):t('drawer.skill')));
|
|
807
837
|
b.append(el('div','d-title', obj.title||obj.name));
|
|
808
|
-
const sec=el('div','d-section'); sec.append(el('div','d-label','
|
|
809
|
-
const body=el('div','md-body'); body.append(el('div','empty','
|
|
838
|
+
const sec=el('div','d-section'); sec.append(el('div','d-label',t('drawer.file',{rel})));
|
|
839
|
+
const body=el('div','md-body'); body.append(el('div','empty',t('drawer.loading')));
|
|
810
840
|
sec.append(body); b.append(sec);
|
|
811
841
|
$('#drawer').setAttribute('aria-hidden','false');
|
|
812
842
|
try{
|
|
813
843
|
const r=await fetch('/api/agentfile?path='+encodeURIComponent(rel));
|
|
814
844
|
const data=await r.json().catch(()=>({}));
|
|
815
|
-
if(!r.ok){ body.innerHTML=''; body.append(el('div','empty', data.error||'
|
|
845
|
+
if(!r.ok){ body.innerHTML=''; body.append(el('div','empty', data.error||t('drawer.loadError'))); return; }
|
|
816
846
|
body.innerHTML=mdLite(data.content||'');
|
|
817
847
|
}catch(err){
|
|
818
|
-
body.innerHTML=''; body.append(el('div','empty','
|
|
848
|
+
body.innerHTML=''; body.append(el('div','empty',t('drawer.loadError')));
|
|
819
849
|
}
|
|
820
850
|
}
|
|
821
851
|
|
|
@@ -836,11 +866,11 @@ function infoRow(label,value){
|
|
|
836
866
|
return row;
|
|
837
867
|
}
|
|
838
868
|
function statTile(value,label,sub){
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if(sub)
|
|
843
|
-
return
|
|
869
|
+
const tile=el('div','stat-tile');
|
|
870
|
+
tile.append(el('div','stat-tile-val',String(value)));
|
|
871
|
+
tile.append(el('div','stat-tile-label',label));
|
|
872
|
+
if(sub) tile.append(el('div','stat-tile-sub',sub));
|
|
873
|
+
return tile;
|
|
844
874
|
}
|
|
845
875
|
function renderInfo(){
|
|
846
876
|
const box=$('#infoGrid'); if(!box) return;
|
|
@@ -852,39 +882,39 @@ function renderInfo(){
|
|
|
852
882
|
|
|
853
883
|
// Project: mode/language/agent/type from config
|
|
854
884
|
const projRows=el('div','info-rows');
|
|
855
|
-
projRows.append(infoRow('
|
|
856
|
-
projRows.append(infoRow('
|
|
857
|
-
projRows.append(infoRow('
|
|
858
|
-
projRows.append(infoRow('
|
|
859
|
-
box.append(infoSection('
|
|
885
|
+
projRows.append(infoRow(t('info.projectType'), c.projectType||'—'));
|
|
886
|
+
projRows.append(infoRow(t('info.mode'), c.mode||'—'));
|
|
887
|
+
projRows.append(infoRow(t('info.language'), c.language||'—'));
|
|
888
|
+
projRows.append(infoRow(t('info.activeAgent'), c.agent||'—'));
|
|
889
|
+
box.append(infoSection(t('info.project'),'info',projRows));
|
|
860
890
|
|
|
861
891
|
// Runners: agent → command, monospace
|
|
862
892
|
const runners=c.runners||{};
|
|
863
893
|
const runnerKeys=Object.keys(runners);
|
|
864
894
|
const runnerRows=el('div','info-rows info-rows-mono');
|
|
865
|
-
if(!runnerKeys.length) runnerRows.append(el('div','empty','
|
|
895
|
+
if(!runnerKeys.length) runnerRows.append(el('div','empty',t('info.noRunners')));
|
|
866
896
|
runnerKeys.forEach(k=> runnerRows.append(infoRow(k, runners[k])));
|
|
867
|
-
box.append(infoSection('
|
|
897
|
+
box.append(infoSection(t('info.runners'),'run',runnerRows));
|
|
868
898
|
|
|
869
899
|
// Counts: tasks/specs/agents/skills/enabled workflow steps
|
|
870
900
|
const tiles=el('div','stat-tiles');
|
|
871
|
-
tiles.append(statTile(`${s.done}/${s.total}`,'
|
|
872
|
-
tiles.append(statTile(String((P.specs||[]).length),'
|
|
873
|
-
tiles.append(statTile(String((P.agents||[]).length),'
|
|
874
|
-
tiles.append(statTile(String((P.skills||[]).length),'
|
|
875
|
-
tiles.append(statTile(`${enabledSteps.length}/${steps.length}`,'
|
|
876
|
-
box.append(infoSection('
|
|
901
|
+
tiles.append(statTile(`${s.done}/${s.total}`,t('info.tasksLabel'),t('info.doneSub',{pct:s.pct})));
|
|
902
|
+
tiles.append(statTile(String((P.specs||[]).length),t('info.specsLabel'),t('info.filesSub')));
|
|
903
|
+
tiles.append(statTile(String((P.agents||[]).length),t('info.agentsLabel'),t('info.personasSub')));
|
|
904
|
+
tiles.append(statTile(String((P.skills||[]).length),t('info.skillsLabel'),t('info.proceduresSub')));
|
|
905
|
+
tiles.append(statTile(`${enabledSteps.length}/${steps.length}`,t('info.workflowLabel'),t('info.stepsEnabledSub')));
|
|
906
|
+
box.append(infoSection(t('info.counts'),'board',tiles));
|
|
877
907
|
|
|
878
908
|
// Specs: the P.specs filename list
|
|
879
909
|
const specsList=el('ul','flatlist');
|
|
880
910
|
const specs=P.specs||[];
|
|
881
|
-
if(!specs.length) specsList.append(li('empty','
|
|
911
|
+
if(!specs.length) specsList.append(li('empty',t('board.noneYet')));
|
|
882
912
|
specs.forEach(sp=> specsList.append(li(null,sp)));
|
|
883
|
-
box.append(infoSection('
|
|
913
|
+
box.append(infoSection(t('info.specsLabel'),'backlog',specsList));
|
|
884
914
|
|
|
885
915
|
// Workflow: compact list of enabled steps (name + cap/skill)
|
|
886
916
|
const wfList=el('div','info-wf-list');
|
|
887
|
-
if(!enabledSteps.length) wfList.append(el('div','empty','
|
|
917
|
+
if(!enabledSteps.length) wfList.append(el('div','empty',t('info.noEnabledSteps')));
|
|
888
918
|
enabledSteps.forEach(st=>{
|
|
889
919
|
const row=el('div','info-wf-row');
|
|
890
920
|
row.append(el('span','info-wf-name',st.name));
|
|
@@ -892,42 +922,44 @@ function renderInfo(){
|
|
|
892
922
|
if(meta) row.append(el('span','info-wf-meta',meta));
|
|
893
923
|
wfList.append(row);
|
|
894
924
|
});
|
|
895
|
-
box.append(infoSection('
|
|
925
|
+
box.append(infoSection(t('info.workflowLabel'),'workflow',wfList));
|
|
896
926
|
}
|
|
897
927
|
|
|
898
928
|
function openDrawer(id,keep){
|
|
899
|
-
|
|
929
|
+
// named `task`, not `t` — `t` is the global translation function (see i18n.js) and this whole
|
|
930
|
+
// function calls it repeatedly below; shadowing it with a task variable would break every call.
|
|
931
|
+
const task=allTasks().find(x=>x.id===id); if(!task) return;
|
|
900
932
|
openTaskId=id;
|
|
901
933
|
if(!keep && taskFromPath()!==id) history.pushState(null,'','/'+activeTab+'/'+encodeURIComponent(id));
|
|
902
934
|
const b=$('#drawerBody'); const prev=keep?$('.drawer-panel').scrollTop:0; b.innerHTML='';
|
|
903
|
-
b.append(el('div','d-id',
|
|
904
|
-
b.append(el('div','d-title',
|
|
905
|
-
const sSec=el('div','d-section'); sSec.append(el('div','d-label','
|
|
935
|
+
b.append(el('div','d-id',task.id+' · '+(task.level||'standard')+' · '+task.file));
|
|
936
|
+
b.append(el('div','d-title',task.title));
|
|
937
|
+
const sSec=el('div','d-section'); sSec.append(el('div','d-label',t('task.status')));
|
|
906
938
|
const sr=el('div','status-row');
|
|
907
939
|
Object.keys(STATUS).forEach(k=>{
|
|
908
|
-
const btn=el('button','status-btn'+(
|
|
909
|
-
if(
|
|
940
|
+
const btn=el('button','status-btn'+(task.status===k?' active':''),STATUS[k]);
|
|
941
|
+
if(task.status===k) btn.style.background=cssv('--s-'+k);
|
|
910
942
|
btn.addEventListener('click',()=>patchTask(id,{status:k}));
|
|
911
943
|
sr.append(btn);
|
|
912
944
|
});
|
|
913
945
|
sSec.append(sr); b.append(sSec);
|
|
914
946
|
|
|
915
947
|
const tr=runtimeTests(id);
|
|
916
|
-
if(tr){ const ts=el('div','d-section'); ts.append(el('div','d-label','
|
|
917
|
-
ts.append(el('div','d-text', tr.failed
|
|
948
|
+
if(tr){ const ts=el('div','d-section'); ts.append(el('div','d-label',t('task.tests')));
|
|
949
|
+
ts.append(el('div','d-text', tr.failed?t('task.failingPassing',{f:tr.failed,p:tr.passed||0}):t('task.passing',{n:tr.passed||0}))); b.append(ts); }
|
|
918
950
|
|
|
919
|
-
const cSec=el('div','d-section'); cSec.append(el('div','d-label','
|
|
951
|
+
const cSec=el('div','d-section'); cSec.append(el('div','d-label',t('task.comments')));
|
|
920
952
|
const list=el('div','comments');
|
|
921
|
-
(
|
|
922
|
-
if(!(
|
|
953
|
+
(task.comments||[]).forEach(cm=> list.append(el('div','comment',cm)));
|
|
954
|
+
if(!(task.comments||[]).length) list.append(el('div','empty',t('task.noComments')));
|
|
923
955
|
cSec.append(list);
|
|
924
|
-
const box=el('div','c-box'); const ta=el('textarea'); ta.placeholder='
|
|
956
|
+
const box=el('div','c-box'); const ta=el('textarea'); ta.placeholder=t('task.addCommentPlaceholder');
|
|
925
957
|
const actions=el('div','c-actions');
|
|
926
|
-
const add=el('button','btn','
|
|
958
|
+
const add=el('button','btn',t('action.add')); const an=el('button','btn primary',t('action.addToAnalyze'));
|
|
927
959
|
add.addEventListener('click',()=>{ if(ta.value.trim())addComment(id,ta.value.trim(),'note'); });
|
|
928
960
|
an.addEventListener('click',()=>{ if(ta.value.trim())addComment(id,ta.value.trim(),'analyze'); });
|
|
929
961
|
actions.append(add,an); box.append(ta,actions);
|
|
930
|
-
box.append(el('div','empty','
|
|
962
|
+
box.append(el('div','empty',t('task.toAnalyzeHint')));
|
|
931
963
|
cSec.append(box); b.append(cSec);
|
|
932
964
|
$('#drawer').setAttribute('aria-hidden','false');
|
|
933
965
|
if(keep) $('.drawer-panel').scrollTop=prev;
|
|
@@ -994,8 +1026,12 @@ $$('.attn-filters .fchip').forEach(b=> b.addEventListener('click',()=>{ attnFilt
|
|
|
994
1026
|
// settings — the footer link opens the Settings tab; selects save on change
|
|
995
1027
|
const footerSettingsBtn=$('#footerSettings'); if(footerSettingsBtn) footerSettingsBtn.addEventListener('click',()=>navigateTab('settings'));
|
|
996
1028
|
const footerLogo=$('.footer-logo'); if(footerLogo) footerLogo.addEventListener('click',e=>{ e.preventDefault(); navigateTab('board'); });
|
|
997
|
-
|
|
998
|
-
|
|
1029
|
+
function onModeSelectChange(e){ setModeSelects(e.target.value); saveSettings(); }
|
|
1030
|
+
function onLangSelectChange(e){ setLangSelect(e.target.value); saveSettings(); }
|
|
1031
|
+
$('#setMode').addEventListener('change',onModeSelectChange);
|
|
1032
|
+
$('#setLang').addEventListener('change',onLangSelectChange);
|
|
1033
|
+
const topModeSel=$('#topMode'); if(topModeSel) topModeSel.addEventListener('change',onModeSelectChange);
|
|
1034
|
+
const topLangSel=$('#topLang'); if(topLangSel) topLangSel.addEventListener('change',onLangSelectChange);
|
|
999
1035
|
const setDesignSel=$('#setDesign'); if(setDesignSel) setDesignSel.addEventListener('change',()=>saveDesign(setDesignSel.value));
|
|
1000
1036
|
// theme
|
|
1001
1037
|
(function(){ const s=localStorage.getItem('spf-theme'); if(s)document.documentElement.setAttribute('data-theme',s);
|