diff --git a/Plugin/unraid/include/ai_chat.php b/Plugin/unraid/include/ai_chat.php index 168580f..4a2b15a 100644 --- a/Plugin/unraid/include/ai_chat.php +++ b/Plugin/unraid/include/ai_chat.php @@ -194,7 +194,11 @@ function vv_ai_chat_assets(): void { .vv-ai-think.open { display:block; } .vv-ai-src { margin-top:9px; border-top:1px solid #1c1c1c; padding-top:7px; } -.vv-ai-src-h { font-size:9px; letter-spacing:.07em; text-transform:uppercase; color:#3a3a3a; margin-bottom:4px; } +.vv-ai-src-h { font-size:9px; letter-spacing:.07em; text-transform:uppercase; color:#3a3a3a; + margin-bottom:4px; cursor:pointer; user-select:none; } +.vv-ai-src-h:hover { color:#6a6a6a; } +.vv-ai-src-car { display:inline-block; width:10px; } +.vv-ai-src.collapsed .vv-ai-src-l { display:none; } .vv-ai-src-i { font-size:11px; color:#5a5a5a; padding:2px 0; cursor:pointer; display:flex; gap:8px; } .vv-ai-src-i:hover { color:#8a8a8a; } .vv-ai-src-n { color:#3a4a6a; font-family:monospace; flex-shrink:0; } @@ -556,7 +560,12 @@ vv_ai_profiles_script(); // path carrying a quote interpolated into an attribute is code execution, not a display bug. function sourcesHtml(sources) { if (!sources || !sources.length) return ''; - let h = '
Sources
'; + // Expanded by default and collapsible by choice, never the reverse. What the model was given + // is the first thing worth seeing when an answer is wrong, and hiding it behind a click makes + // a bad retrieval look like a bad model. + let h = `
` + + `Sources (${sources.length})
` + + `
`; sources.forEach((s, i) => { // A web result is a page on the internet, not a file in this install: it opens in a tab // rather than in the source viewer, it leads with its title rather than its URL, and it @@ -573,7 +582,7 @@ vv_ai_profiles_script(); + `[${i+1}]${esc(label)}` + `${Number(s.score).toFixed(3)}
`; }); - return h + '
'; + return h + '
'; } function addAnswer(job) { @@ -623,7 +632,14 @@ vv_ai_profiles_script(); if (u) window.open(u, '_blank', 'noopener,noreferrer'); return; } - if (src && src.dataset.src) { vvAiOpen(src.dataset.src); return; } + // A page that can do something better with a source than show it gets first refusal — the + // Scheduler opens scripts in its editor, where they can actually be changed. Everything + // else falls through to the read-only viewer. + if (src && src.dataset.src) { + if (o.onOpenSource) o.onOpenSource(src.dataset.src); + else vvAiOpen(src.dataset.src); + return; + } const cite = e.target.closest('.vv-ai-cite'); if (cite) { const s = lastSources[Number(cite.dataset.cite) - 1]; @@ -632,9 +648,23 @@ vv_ai_profiles_script(); if (u) window.open(u, '_blank', 'noopener,noreferrer'); return; } - if (s && s.path) vvAiOpen(s.path); + // Same routing as a source row — a citation and the row it refers to must not behave + // differently, or clicking [3] and clicking source 3 land in different places. + if (s && s.path) { + if (o.onOpenSource) o.onOpenSource(s.path); + else vvAiOpen(s.path); + } return; } + const stog = e.target.closest('[data-src-toggle]'); + if (stog) { + const box = stog.parentElement; + box.classList.toggle('collapsed'); + const car = stog.querySelector('.vv-ai-src-car'); + if (car) car.textContent = box.classList.contains('collapsed') ? '▸' : '▾'; + return; + } + const off = e.target.closest('[data-offer]'); if (off) { answerOffer(+off.dataset.offer, off.dataset.yes === '1'); return; } diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index 946a32b..27faff1 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -2444,6 +2444,7 @@ if (document.getElementById('vv-sched-ai-chat')) { onOffer: (kind, yes) => { if (kind === 'fix') vvAiFixAnswer(yes); }, // Per-block Insert buttons render only where a page can receive them. onInsertCode: vvAiInsertCode, + onOpenSource: vvAiOpenSource, }); } @@ -2510,6 +2511,19 @@ let vvAiFixAsk = null; // { scope, symptom } while the offer is open and awa // It is an offer, never automatic: the editor usually holds work in progress, and a reply that // silently rewrote it would be far worse than copy/paste. +// Clicking a retrieved source here opens it where it can be changed, not just read. The component +// falls back to its read-only viewer for anything this declines — which is everything that is not +// a script, since api/script.php serves scripts and the index also carries READMEs and manuals. +function vvAiOpenSource(path) { + if (!path) return; + if (/\.sh$/.test(path)) { + vvEditScript(path); + if (vvSchedChat) vvSchedChat.note('Opened ' + path + ' in the editor.'); + return; + } + vvAiOpen(path); +} + // display:'' vs 'none' is how this panel switches views — see vvShowEditor and friends. function vvAiEditorOpen() { const ed = document.getElementById('vv-editor');