添加复制按钮
This commit is contained in:
parent
d3c8841d01
commit
f40c01b078
218
scripts/llm.js
218
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() {
|
async function getLLMPassword() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
chrome.storage.local.get(["llmPassword"], (res) => {
|
chrome.storage.local.get(["llmPassword"], (res) => {
|
||||||
@ -107,11 +168,10 @@ async function getLLMWorkspaceSlug(token, forceRefresh = false) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅插入按钮,不绑定事件
|
// 代码块复制按钮插入
|
||||||
function wrapHtmlWithCopyBtn(html) {
|
function wrapHtmlWithCopyBtn(html) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.innerHTML = html;
|
wrapper.innerHTML = html;
|
||||||
|
|
||||||
wrapper.querySelectorAll('pre > code').forEach(codeEl => {
|
wrapper.querySelectorAll('pre > code').forEach(codeEl => {
|
||||||
const pre = codeEl.parentElement;
|
const pre = codeEl.parentElement;
|
||||||
if (pre.querySelector('.copy-btn')) return;
|
if (pre.querySelector('.copy-btn')) return;
|
||||||
@ -124,16 +184,12 @@ function wrapHtmlWithCopyBtn(html) {
|
|||||||
return wrapper.innerHTML;
|
return wrapper.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染后批量绑定复制事件
|
|
||||||
function bindAllCopyButtons(container) {
|
function bindAllCopyButtons(container) {
|
||||||
(container || document).querySelectorAll('.llm-md .copy-btn').forEach(btn => {
|
(container || document).querySelectorAll('.llm-md .copy-btn').forEach(btn => {
|
||||||
// 避免重复绑定
|
|
||||||
if (btn.dataset.bind === "true") return;
|
if (btn.dataset.bind === "true") return;
|
||||||
btn.dataset.bind = "true";
|
btn.dataset.bind = "true";
|
||||||
|
|
||||||
btn.addEventListener('click', function (e) {
|
btn.addEventListener('click', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// 只复制紧邻 code 元素的内容
|
|
||||||
const codeEl = btn.parentElement.querySelector('code');
|
const codeEl = btn.parentElement.querySelector('code');
|
||||||
const codeText = codeEl ? (codeEl.innerText || codeEl.textContent) : "";
|
const codeText = codeEl ? (codeEl.innerText || codeEl.textContent) : "";
|
||||||
let success = false, error = null;
|
let success = false, error = null;
|
||||||
@ -159,7 +215,6 @@ function bindAllCopyButtons(container) {
|
|||||||
btn.innerText = '失败';
|
btn.innerText = '失败';
|
||||||
setTimeout(() => btn.innerText = '复制', 1200);
|
setTimeout(() => btn.innerText = '复制', 1200);
|
||||||
}
|
}
|
||||||
// 必然有日志
|
|
||||||
if (success) {
|
if (success) {
|
||||||
log("[复制代码] 成功,内容:", codeText);
|
log("[复制代码] 成功,内容:", codeText);
|
||||||
} else {
|
} 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) {
|
async function fetchLLMAnswer(question, container, contentDiv) {
|
||||||
if (!question || !question.trim()) return;
|
if (!question || !question.trim()) return;
|
||||||
|
|
||||||
@ -187,6 +310,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
contentDiv.scrollTop = contentDiv.scrollHeight;
|
contentDiv.scrollTop = contentDiv.scrollHeight;
|
||||||
|
|
||||||
let collectedSources = [];
|
let collectedSources = [];
|
||||||
|
let jumpMap = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = await getLLMToken();
|
const token = await getLLMToken();
|
||||||
@ -259,8 +383,9 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
msgBox.innerHTML =
|
msgBox.innerHTML =
|
||||||
(thinkStr ? `<div class="llm-think">${thinkStr}</div>` : "") +
|
(thinkStr ? `<div class="llm-think">${thinkStr}</div>` : "") +
|
||||||
`<span class="assistant-label">智能助手:</span><div class="llm-md">${wrapHtmlWithCopyBtn(html)}</div>`;
|
`<span class="assistant-label">智能助手:</span><div class="llm-md">${wrapHtmlWithCopyBtn(html)}</div>`;
|
||||||
// 【关键】渲染后绑定复制事件
|
|
||||||
bindAllCopyButtons(msgBox);
|
bindAllCopyButtons(msgBox);
|
||||||
|
insertCopyFullAnswerBtn(msgBox, answerStr);
|
||||||
|
|
||||||
contentDiv.scrollTop = contentDiv.scrollHeight;
|
contentDiv.scrollTop = contentDiv.scrollHeight;
|
||||||
}
|
}
|
||||||
@ -268,84 +393,23 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染参考资料并再次插入复制按钮
|
||||||
if (collectedSources.length > 0) {
|
if (collectedSources.length > 0) {
|
||||||
|
const titles = collectedSources.map(s => s.title).filter(Boolean);
|
||||||
|
jumpMap = await resolveJumpLinks(titles);
|
||||||
|
|
||||||
const sourceList = document.createElement("div");
|
const sourceList = document.createElement("div");
|
||||||
sourceList.className = "llm-sources";
|
sourceList.className = "llm-sources";
|
||||||
sourceList.style.marginTop = "10px";
|
sourceList.style.marginTop = "10px";
|
||||||
sourceList.style.fontSize = "13px";
|
sourceList.style.fontSize = "13px";
|
||||||
sourceList.style.color = "#555";
|
sourceList.style.color = "#555";
|
||||||
msgBox.appendChild(sourceList);
|
msgBox.appendChild(sourceList);
|
||||||
await renderSources(sourceList, collectedSources);
|
await renderSources(sourceList, collectedSources, jumpMap);
|
||||||
|
|
||||||
|
insertCopyFullAnswerBtn(msgBox, answerStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msgBox.innerHTML += `<span style='color:red;'>LLM异常:${e}</span>`;
|
msgBox.innerHTML += `<span style='color:red;'>LLM异常:${e}</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user