diff --git a/src/cfg/ui/server.py b/src/cfg/ui/server.py index f131ef4..a3491d8 100644 --- a/src/cfg/ui/server.py +++ b/src/cfg/ui/server.py @@ -620,6 +620,9 @@ def find_free_port(host: str = DEFAULT_HOST) -> int: .docbody{padding:14px 16px;white-space:pre-wrap;word-break:break-word;line-height:1.6;max-height:none} .spin{padding:30px;color:var(--faint);font-size:13px;text-align:center} + .spinner{display:inline-block;width:11px;height:11px;margin-right:6px;vertical-align:-1px; + border:2px solid var(--edge2);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite} + @keyframes spin{to{transform:rotate(360deg)}} .toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(8px);background:var(--panel2); border:1px solid var(--edge2);border-radius:10px;padding:10px 18px;font-size:13px;z-index:60; box-shadow:var(--shadow);opacity:0;transition:all .18s;pointer-events:none;display:flex;align-items:center;gap:9px} @@ -1219,15 +1222,20 @@ def find_free_port(host: str = DEFAULT_HOST) -> int: $("diff").innerHTML=`
computing diff…
`; const res=await api("diff",{record:S.sel,a:"=HEAD",b:"=live"}); renderDiff(res,"recorded","live",S.diffCtx.empty); - $("aImpact").onclick=()=>showImpact(); + // The top button adds LLM narration on demand; the deterministic panel is shown by default below. + $("aImpact").onclick=()=>showImpact({llm:true}); $("aAdopt").onclick=()=>openAdopt(rec); refreshImpactBtn(); + showImpact(); // auto-show the deterministic system-impact panel (no LLM call) } -async function showImpact(){ +async function showImpact(opts){ if(!S.diffCtx)return; + const useLlm=!!(opts&&opts.llm); // deterministic by default; LLM narration only on request const against=[...S.against].filter(k=>k!==S.sel); - const btn=$("aImpact"); if(btn){btn.disabled=true;btn.textContent="Analyzing…";} - const payload={record:S.sel,a:S.diffCtx.a,b:S.diffCtx.b,use_llm:true}; + const btn=$("aImpact"); if(btn){btn.disabled=true;btn.innerHTML=`${useLlm?"Narrating…":"Analyzing…"}`;} + // The narration call takes a few seconds; give the clicked in-panel button its own busy state. + const busyBtn=$("aNarrate"); if(busyBtn){busyBtn.disabled=true;busyBtn.innerHTML=`Narrating…`;} + const payload={record:S.sel,a:S.diffCtx.a,b:S.diffCtx.b,use_llm:useLlm}; if(S.impact&&S.impact.provider)payload.provider=S.impact.provider; if(S.impact&&S.impact.model)payload.model=S.impact.model; if(against.length)payload.against=against; @@ -1257,6 +1265,11 @@ def find_free_port(host: str = DEFAULT_HOST) -> int: if(ov.unknowns&&(Array.isArray(ov.unknowns)?ov.unknowns.length:true))parts.push(`
unknowns${asList(ov.unknowns)}
`); const fallback=(!parts.length&&(llm.text||llm.narration))?`
${esc(llm.text||llm.narration)}
`:""; llmHtml=`
${esc(llm.provider||"llm")} · ${esc(llm.model||"")} narration
${parts.join("")||fallback||`
no narration returned
`}
`; + } else if(!useLlm){ + // deterministic-only run (the default): the impact above is computed locally. Narration + // is opt-in and DOES send the record diff to the configured provider — say so plainly. + const prov=(S.impact&&S.impact.provider)?esc(S.impact.provider):"the configured provider"; + llmHtml=`
LLM narration
The impact above is computed locally. Narration sends this record's diff to ${prov}.
`; } else { llmHtml=`
LLM narration
off — enable [connections] in .cfg.toml for a written explanation of what this change does to the system. Everything above is computed locally, no data leaves your machine.
`; } @@ -1280,6 +1293,7 @@ def find_free_port(host: str = DEFAULT_HOST) -> int: const wrap=$("diff"); const existing=wrap.querySelector(".impact"); if(existing)existing.remove(); wrap.insertAdjacentHTML("afterbegin",panel); + const nb=$("aNarrate"); if(nb)nb.onclick=()=>showImpact({llm:true}); } async function selectNode(e){ if(!e)return;markNode(e.seq,false); @@ -1296,7 +1310,7 @@ def find_free_port(host: str = DEFAULT_HOST) -> int: const res=await api("diff",{record:S.sel,a:"@"+parent.seq,b:"@"+e.seq}); renderDiff(res,"@"+parent.seq,"@"+e.seq,"No field-level change from the parent version.");} else{S.diffCtx=null;const res=await api("show",{record:S.sel,ref:"@"+e.seq});renderDoc(res);} - const ib=$("aImpact");if(ib)ib.onclick=()=>showImpact(); + const ib=$("aImpact");if(ib)ib.onclick=()=>showImpact({llm:true}); const rb=$("aRestore");if(rb)rb.onclick=()=>openRestore(S.records.find(r=>r.collection+":"+r.record_id===S.sel),"@"+e.seq); refreshImpactBtn(); }