From f40c01b078c35ad1577af8393ca7d3cf0ebfb430 Mon Sep 17 00:00:00 2001 From: luke Date: Sun, 1 Jun 2025 23:31:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=8D=E5=88=B6=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/llm.js | 218 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 141 insertions(+), 77 deletions(-) diff --git a/scripts/llm.js b/scripts/llm.js index fc8f051..4f24ef5 100644 --- a/scripts/llm.js +++ b/scripts/llm.js @@ -42,6 +42,67 @@ async function resolveJumpLinks(titles) { }); } +async function renderSources(sourceList, sources, jumpMap) { + sourceList.style.marginTop = "12px"; + sourceList.style.borderTop = "1px solid #dbe3f3"; + sourceList.style.paddingTop = "7px"; + + const titleBar = document.createElement("div"); + titleBar.textContent = "📎 参考资料"; + titleBar.style.fontWeight = "bold"; + titleBar.style.marginBottom = "6px"; + titleBar.style.fontSize = "13.5px"; + titleBar.style.color = "#0d2fec"; + sourceList.appendChild(titleBar); + + // ul 包裹所有参考资料 + const ul = document.createElement("ul"); + ul.style.paddingLeft = "1.2em"; + ul.style.margin = "0"; + ul.style.listStyle = "disc"; + ul.style.fontSize = "14px"; + + for (const src of sources) { + const url = jumpMap[src.title]; + const li = document.createElement("li"); + li.style.marginBottom = "3px"; + li.style.lineHeight = "1.7"; + + let scoreText = ""; + if (typeof src.score === "number") { + scoreText = `(相似度:${(src.score * 100).toFixed(2)}%)`; + } + + if (url) { + const link = document.createElement("a"); + link.href = url; + link.textContent = src.title || "未命名文档"; + link.target = "_blank"; + link.style.color = "#2560de"; + link.style.textDecoration = "none"; + link.onmouseenter = () => (link.style.textDecoration = "underline"); + link.onmouseleave = () => (link.style.textDecoration = "none"); + li.appendChild(link); + + if (scoreText) { + const scoreSpan = document.createElement("span"); + scoreSpan.style.color = "#8990a1"; + scoreSpan.style.fontSize = "12.5px"; + scoreSpan.style.marginLeft = "6px"; + scoreSpan.textContent = scoreText; + li.appendChild(scoreSpan); + } + } else { + li.textContent = (src.title || "未命名文档") + (scoreText ? " " + scoreText : ""); + li.style.color = "#666"; + } + + ul.appendChild(li); + } + + sourceList.appendChild(ul); +} + async function getLLMPassword() { return new Promise((resolve) => { chrome.storage.local.get(["llmPassword"], (res) => { @@ -107,11 +168,10 @@ async function getLLMWorkspaceSlug(token, forceRefresh = false) { }); } -// 仅插入按钮,不绑定事件 +// 代码块复制按钮插入 function wrapHtmlWithCopyBtn(html) { const wrapper = document.createElement('div'); wrapper.innerHTML = html; - wrapper.querySelectorAll('pre > code').forEach(codeEl => { const pre = codeEl.parentElement; if (pre.querySelector('.copy-btn')) return; @@ -124,16 +184,12 @@ function wrapHtmlWithCopyBtn(html) { return wrapper.innerHTML; } -// 渲染后批量绑定复制事件 function bindAllCopyButtons(container) { (container || document).querySelectorAll('.llm-md .copy-btn').forEach(btn => { - // 避免重复绑定 if (btn.dataset.bind === "true") return; btn.dataset.bind = "true"; - btn.addEventListener('click', function (e) { e.stopPropagation(); - // 只复制紧邻 code 元素的内容 const codeEl = btn.parentElement.querySelector('code'); const codeText = codeEl ? (codeEl.innerText || codeEl.textContent) : ""; let success = false, error = null; @@ -159,7 +215,6 @@ function bindAllCopyButtons(container) { btn.innerText = '失败'; setTimeout(() => btn.innerText = '复制', 1200); } - // 必然有日志 if (success) { log("[复制代码] 成功,内容:", codeText); } else { @@ -170,8 +225,76 @@ function bindAllCopyButtons(container) { }); } +// 复制全部回答按钮,从HTML解析ul/li/a结构 +function insertCopyFullAnswerBtn(msgBox, answerStr) { + if (msgBox.querySelector('.copy-full-answer-btn')) return; -// fetchLLMAnswer,支持流式markdown、代码块复制按钮 + const copyBtn = document.createElement('button'); + copyBtn.className = 'copy-full-answer-btn'; + copyBtn.innerText = '复制全部回答'; + copyBtn.style.margin = "12px 0 0 0"; + copyBtn.style.display = "block"; + copyBtn.style.background = "#eef1f9"; + copyBtn.style.color = "#2d53a7"; + copyBtn.style.border = "1px solid #dde4ef"; + copyBtn.style.borderRadius = "6px"; + copyBtn.style.padding = "5px 18px"; + copyBtn.style.fontSize = "13.2px"; + copyBtn.style.cursor = "pointer"; + copyBtn.style.float = "right"; + copyBtn.style.position = "relative"; + copyBtn.style.right = "0"; + copyBtn.style.transition = "background 0.13s"; + copyBtn.onmouseenter = () => (copyBtn.style.background = "#dbeafe"); + copyBtn.onmouseleave = () => (copyBtn.style.background = "#eef1f9"); + + copyBtn.onclick = () => { + // 1. 回答正文 + let answerText = (answerStr || "").trim(); + + // 2. 参考资料 markdown(遍历 ul/li/a) + let refArr = []; + const ul = msgBox.querySelector('.llm-sources ul'); + if (ul) { + ul.querySelectorAll('li').forEach(li => { + let link = li.querySelector('a'); + let title = link ? link.textContent : li.textContent; + let url = link ? link.getAttribute('href') : ""; + // 分值(span) + let scoreSpan = li.querySelector('span'); + let scoreText = scoreSpan ? scoreSpan.textContent : ""; + refArr.push(`- [${title}](${url})${scoreText}`); + }); + } + let refText = ""; + if (refArr.length > 0) { + refText = "\n\n参考资料:\n" + refArr.join("\n"); + } + const textToCopy = answerText + refText; + + try { + const textarea = document.createElement("textarea"); + textarea.value = textToCopy; + textarea.setAttribute('readonly', ''); + textarea.style.position = "absolute"; + textarea.style.left = "-9999px"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + copyBtn.innerText = "已复制!"; + setTimeout(() => copyBtn.innerText = "复制全部回答", 1200); + } catch (err) { + copyBtn.innerText = "复制失败"; + setTimeout(() => copyBtn.innerText = "复制全部回答", 1200); + alert("复制失败:" + err); + } + }; + + msgBox.appendChild(copyBtn); +} + +// 主体流式渲染 fetchLLMAnswer async function fetchLLMAnswer(question, container, contentDiv) { if (!question || !question.trim()) return; @@ -187,6 +310,7 @@ async function fetchLLMAnswer(question, container, contentDiv) { contentDiv.scrollTop = contentDiv.scrollHeight; let collectedSources = []; + let jumpMap = {}; try { const token = await getLLMToken(); @@ -259,8 +383,9 @@ async function fetchLLMAnswer(question, container, contentDiv) { msgBox.innerHTML = (thinkStr ? `
${thinkStr}
` : "") + `智能助手:
${wrapHtmlWithCopyBtn(html)}
`; - // 【关键】渲染后绑定复制事件 + bindAllCopyButtons(msgBox); + insertCopyFullAnswerBtn(msgBox, answerStr); contentDiv.scrollTop = contentDiv.scrollHeight; } @@ -268,84 +393,23 @@ async function fetchLLMAnswer(question, container, contentDiv) { } } + // 渲染参考资料并再次插入复制按钮 if (collectedSources.length > 0) { + const titles = collectedSources.map(s => s.title).filter(Boolean); + jumpMap = await resolveJumpLinks(titles); + const sourceList = document.createElement("div"); sourceList.className = "llm-sources"; sourceList.style.marginTop = "10px"; sourceList.style.fontSize = "13px"; sourceList.style.color = "#555"; msgBox.appendChild(sourceList); - await renderSources(sourceList, collectedSources); + await renderSources(sourceList, collectedSources, jumpMap); + + insertCopyFullAnswerBtn(msgBox, answerStr); } } catch (e) { msgBox.innerHTML += `LLM异常:${e}`; } } - -async function renderSources(sourceList, sources) { - const titles = sources.map(s => s.title).filter(Boolean); - const jumpMap = await resolveJumpLinks(titles); - - sourceList.style.marginTop = "12px"; - sourceList.style.borderTop = "1px solid #dbe3f3"; - sourceList.style.paddingTop = "7px"; - - const titleBar = document.createElement("div"); - titleBar.textContent = "📎 参考资料"; - titleBar.style.fontWeight = "bold"; - titleBar.style.marginBottom = "6px"; - titleBar.style.fontSize = "13.5px"; - titleBar.style.color = "#0d2fec"; - sourceList.appendChild(titleBar); - - // 新增ul包裹 - const ul = document.createElement("ul"); - ul.style.paddingLeft = "1.2em"; - ul.style.margin = "0"; - ul.style.listStyle = "disc"; - ul.style.fontSize = "14px"; - - for (const src of sources) { - const url = jumpMap[src.title]; - const li = document.createElement("li"); - li.style.marginBottom = "3px"; - li.style.lineHeight = "1.7"; - - // 分值 - let scoreText = ""; - if (typeof src.score === "number") { - scoreText = `(相似度:${(src.score * 100).toFixed(2)}%)`; - } - - if (url) { - const link = document.createElement("a"); - link.href = url; - link.textContent = src.title || "未命名文档"; - link.target = "_blank"; - link.style.color = "#2560de"; - link.style.textDecoration = "none"; - link.onmouseenter = () => (link.style.textDecoration = "underline"); - link.onmouseleave = () => (link.style.textDecoration = "none"); - li.appendChild(link); - - if (scoreText) { - const scoreSpan = document.createElement("span"); - scoreSpan.style.color = "#8990a1"; - scoreSpan.style.fontSize = "12.5px"; - scoreSpan.style.marginLeft = "6px"; - scoreSpan.textContent = scoreText; - li.appendChild(scoreSpan); - } - } else { - li.textContent = (src.title || "未命名文档") + (scoreText ? " " + scoreText : ""); - li.style.color = "#666"; - } - - ul.appendChild(li); - } - - sourceList.appendChild(ul); -} - -