625 lines
24 KiB
JavaScript
625 lines
24 KiB
JavaScript
async function resolveJumpLinks(titles) {
|
||
if (!titles || titles.length === 0) return {};
|
||
|
||
const config = await loadMergedConfig();
|
||
const filePathCfg = config.apiConfig.filePath;
|
||
const publicBase = config.apiConfig.materialPublicUrl.url;
|
||
const reqField = filePathCfg.requestParams[0].field;
|
||
const respField = filePathCfg.responseParams[0].field;
|
||
|
||
return new Promise((resolveOuter) => {
|
||
chrome.storage.local.get(["token"], async (res) => {
|
||
const token = res.token;
|
||
if (!token) return resolveOuter({});
|
||
try {
|
||
const body = {};
|
||
body[reqField] = titles;
|
||
const resp = await fetch(filePathCfg.url, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"Authorization": token
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
const data = await resp.json();
|
||
const fileMap = data[respField] || {};
|
||
const map = {};
|
||
for (const title of Object.keys(fileMap)) {
|
||
if (fileMap[title]) {
|
||
const cleanPath = fileMap[title].replace(/\.md$/, ".html");
|
||
map[title] = `${cleanPath}`;
|
||
}
|
||
}
|
||
resolveOuter(map);
|
||
} catch (e) {
|
||
resolveOuter({});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
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";
|
||
|
||
// ====== 新增去重处理 ======
|
||
const seen = new Set();
|
||
for (const src of sources) {
|
||
const url = jumpMap[src.title];
|
||
const title = src.title || "未命名文档";
|
||
const scoreText = (typeof src.score === "number") ? `(相似度:${(src.score * 100).toFixed(2)}%)` : "";
|
||
|
||
const key = title + "|" + (url || "");
|
||
if (seen.has(key)) continue; // 跳过重复
|
||
seen.add(key);
|
||
|
||
const li = document.createElement("li");
|
||
li.style.marginBottom = "3px";
|
||
li.style.lineHeight = "1.7";
|
||
|
||
if (url) {
|
||
const link = document.createElement("a");
|
||
link.href = url;
|
||
link.textContent = 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 = title + (scoreText ? " " + scoreText : "");
|
||
li.style.color = "#666";
|
||
}
|
||
|
||
ul.appendChild(li);
|
||
}
|
||
// ====== 去重处理结束 ======
|
||
|
||
sourceList.appendChild(ul);
|
||
}
|
||
|
||
|
||
// === 复制代码块功能 ===
|
||
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;
|
||
const btn = document.createElement('button');
|
||
btn.className = 'copy-btn';
|
||
btn.innerText = '复制';
|
||
pre.appendChild(btn);
|
||
pre.style.position = 'relative';
|
||
});
|
||
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();
|
||
const codeEl = btn.parentElement.querySelector('code');
|
||
const codeText = codeEl ? (codeEl.innerText || codeEl.textContent) : "";
|
||
let success = false, error = null;
|
||
try {
|
||
const textarea = document.createElement("textarea");
|
||
textarea.value = codeText;
|
||
textarea.setAttribute('readonly', '');
|
||
textarea.style.position = "absolute";
|
||
textarea.style.left = "-9999px";
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
success = document.execCommand('copy');
|
||
document.body.removeChild(textarea);
|
||
if (success) {
|
||
btn.innerText = '已复制!';
|
||
setTimeout(() => btn.innerText = '复制', 1200);
|
||
} else {
|
||
btn.innerText = '失败';
|
||
setTimeout(() => btn.innerText = '复制', 1200);
|
||
}
|
||
} catch (err) {
|
||
error = err;
|
||
btn.innerText = '失败';
|
||
setTimeout(() => btn.innerText = '复制', 1200);
|
||
}
|
||
if (!success) {
|
||
alert("复制失败: " + (error ? error : "未知错误"));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function insertCopyFullAnswerBtn(msgBox, answerStr) {
|
||
if (msgBox.querySelector('.copy-full-answer-btn')) return;
|
||
|
||
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) {
|
||
const seen = new Set();
|
||
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 : "";
|
||
// 标识唯一性用 title+url
|
||
let key = title + '|' + url;
|
||
if (!seen.has(key)) {
|
||
refArr.push(`- [${title}](${url})${scoreText}`);
|
||
seen.add(key);
|
||
}
|
||
});
|
||
}
|
||
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);
|
||
}
|
||
|
||
function promptLLMPasswordModal() {
|
||
return new Promise((resolve) => {
|
||
// 避免重复弹窗
|
||
if (document.getElementById("llm-password-modal")) return resolve("");
|
||
|
||
// 创建遮罩层
|
||
const mask = document.createElement("div");
|
||
mask.id = "llm-password-modal-mask";
|
||
mask.style = `
|
||
position: fixed; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.19);
|
||
z-index:9998;`;
|
||
|
||
// 创建弹窗
|
||
const modal = document.createElement("div");
|
||
modal.id = "llm-password-modal";
|
||
modal.style = `
|
||
position: fixed; top: 45%; left: 50%; transform: translate(-50%, -50%);
|
||
background: #fff; border-radius: 10px; box-shadow: 0 8px 38px #0001;
|
||
padding: 24px 32px 18px 32px; z-index:9999; min-width:260px; text-align:center;`;
|
||
modal.innerHTML = `
|
||
<div style="font-size:1rem;margin-bottom:13px;">LLM Token 已过期,请输入新的 LLM 密码:</div>
|
||
<input id="llm-password-input" type="password" style="width:95%;padding:7px 8px;font-size:1rem;border-radius:6px;border:1px solid #bbb;margin-bottom:10px;" autofocus />
|
||
<div>
|
||
<button id="llm-password-ok" style="margin-right:10px;padding:6px 18px;">确定</button>
|
||
<button id="llm-password-cancel" style="padding:6px 18px;">取消</button>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(mask);
|
||
document.body.appendChild(modal);
|
||
|
||
function closeModal(val) {
|
||
document.body.removeChild(mask);
|
||
document.body.removeChild(modal);
|
||
resolve(val);
|
||
}
|
||
|
||
document.getElementById("llm-password-ok").onclick = () => {
|
||
const pwd = document.getElementById("llm-password-input").value;
|
||
closeModal(pwd ? pwd.trim() : "");
|
||
};
|
||
document.getElementById("llm-password-cancel").onclick = () => closeModal("");
|
||
document.getElementById("llm-password-input").onkeydown = (e) => {
|
||
if (e.key === "Enter") document.getElementById("llm-password-ok").click();
|
||
};
|
||
});
|
||
}
|
||
|
||
async function getLLMToken(forceRefresh = false) {
|
||
const config = await loadMergedConfig();
|
||
const auth = await loadUserToken();
|
||
if (!auth) {
|
||
handleLogout(auth);
|
||
return;
|
||
}
|
||
const userToken = auth.token;
|
||
const tokenCfg = config.apiConfig.llmToken;
|
||
const expireMs = config.llmTokenExpireHours * 60 * 60 * 1000;
|
||
|
||
// 检查缓存
|
||
return new Promise((resolve, reject) => {
|
||
chrome.storage.local.get(["llmToken", "llmTokenAt"], async (res) => {
|
||
try {
|
||
const now = Date.now();
|
||
if (!forceRefresh && res.llmToken && res.llmTokenAt && now - res.llmTokenAt < expireMs) {
|
||
return resolve(res.llmToken);
|
||
}
|
||
// 弹窗输入密码
|
||
let pwd = await promptLLMPasswordModal();
|
||
if (!pwd) return reject("未输入密码,无法获取 LLM Token");
|
||
|
||
// 构造请求
|
||
const body = {};
|
||
body[tokenCfg.requestParams[0].field] = pwd;
|
||
let response, text, data;
|
||
try {
|
||
response = await fetch(tokenCfg.url, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"Authorization": `${userToken}`
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
text = await response.text();
|
||
} catch (e) {
|
||
return reject("LLM token接口网络异常: " + e);
|
||
}
|
||
try {
|
||
data = JSON.parse(text);
|
||
} catch (e) {
|
||
return reject("LLM token接口返回非JSON: " + text);
|
||
}
|
||
|
||
const tokenParam = tokenCfg.responseParams.find(p => p.label && p.label.includes("token"));
|
||
const tokenField = tokenParam ? tokenParam.field : "token";
|
||
if (data[tokenField]) {
|
||
chrome.storage.local.set({ llmToken: data[tokenField], llmTokenAt: now });
|
||
return resolve(data[tokenField]);
|
||
} else {
|
||
return reject("LLM token获取失败,响应内容: " + text);
|
||
}
|
||
} catch (e) {
|
||
return reject("getLLMToken代码异常: " + e);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// =========== 智能助手核心 ============
|
||
let isUserAtBottom = true;
|
||
let scrollListenerInited = false;
|
||
|
||
async function fetchLLMAnswer(question, container, contentDiv) {
|
||
if (!question || !question.trim()) return;
|
||
|
||
// 历史:用户输入时追加
|
||
appendLlmHistory({ role: "user", text: question });
|
||
|
||
// 获取Token
|
||
let llmToken;
|
||
try {
|
||
// llmToken = await getLLMToken();
|
||
llmToken = "82c16297e76d95dce8395e8d9d2ebe27cad2893ae168f9c3eee7304b39632ed3";
|
||
} catch (e) {
|
||
alert("LLM Token 获取失败:" + e);
|
||
return;
|
||
}
|
||
|
||
const config = await loadMergedConfig();
|
||
const auth = await loadUserToken();
|
||
if (!auth) {
|
||
handleLogout(auth);
|
||
return;
|
||
}
|
||
const userToken = auth.token;
|
||
const url = config.apiConfig.llmChat.url;
|
||
|
||
// ... 渲染用户输入 ...
|
||
const userItem = document.createElement("div");
|
||
userItem.className = "llm-msg-item llm-user";
|
||
userItem.innerHTML = `<span style="color:#2a63c8;font-weight:bold;">我:</span> ${question}`;
|
||
contentDiv.appendChild(userItem);
|
||
container.classList.remove("hidden");
|
||
|
||
const msgBox = document.createElement("div");
|
||
msgBox.className = "llm-msg-item llm-assistant";
|
||
contentDiv.appendChild(msgBox);
|
||
|
||
// 只绑定一次滚动监听,且在首次流式渲染时保证已绑定
|
||
const resultContent = document.getElementById("llmResultContent");
|
||
if (resultContent && !scrollListenerInited) {
|
||
resultContent.addEventListener("scroll", function() {
|
||
isUserAtBottom = resultContent.scrollTop + resultContent.clientHeight >= resultContent.scrollHeight - 20;
|
||
});
|
||
scrollListenerInited = true;
|
||
}
|
||
|
||
let collectedSources = [];
|
||
let jumpMap = {};
|
||
|
||
// LLM 问答接口流式处理
|
||
try {
|
||
const body = { llmToken, question };
|
||
|
||
const res = await fetch(url, {
|
||
method: "POST",
|
||
headers: {
|
||
"Authorization": `${userToken}`,
|
||
"Accept": "text/event-stream",
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
if (!res.ok || !res.body) {
|
||
msgBox.innerHTML = "<span style='color:red;'>LLM接口请求失败</span>";
|
||
return;
|
||
}
|
||
|
||
const reader = res.body.getReader();
|
||
const decoder = new TextDecoder("utf-8");
|
||
let buffer = "", done = false;
|
||
let thinkStr = "", answerStr = "", inThink = false;
|
||
|
||
while (!done) {
|
||
const { value, done: readerDone } = await reader.read();
|
||
done = readerDone;
|
||
if (value) {
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split("\n\n");
|
||
buffer = lines.pop(); // 留给下次未完整数据
|
||
|
||
for (let line of lines) {
|
||
line = line.trim();
|
||
if (!line.startsWith("data:")) continue;
|
||
|
||
const chunkStr = line.slice(5).trim();
|
||
if (!chunkStr) continue;
|
||
let chunk;
|
||
try {
|
||
chunk = JSON.parse(chunkStr);
|
||
} catch (err) {
|
||
console.warn("非法 JSON:", chunkStr);
|
||
continue;
|
||
}
|
||
|
||
// 收集 sources
|
||
if (chunk.sources) {
|
||
collectedSources = chunk.sources;
|
||
}
|
||
|
||
// 只要有 textResponse 就累加
|
||
let content = chunk.textResponse ?? "";
|
||
if (content.includes("<think>")) {
|
||
inThink = true;
|
||
content = content.replace("<think>", "");
|
||
}
|
||
if (content.includes("</think>")) {
|
||
inThink = false;
|
||
content = content.replace("</think>", "");
|
||
}
|
||
|
||
if (inThink) {
|
||
thinkStr += content.replace(/\n/g, "<br>");
|
||
} else {
|
||
answerStr += content;
|
||
}
|
||
|
||
// 渲染
|
||
let html = "";
|
||
try {
|
||
html = window.marked ? wrapHtmlWithCopyBtn(marked.parse(answerStr)) : answerStr.replace(/\n/g, "<br>");
|
||
} catch (e) {
|
||
html = answerStr.replace(/\n/g, "<br>");
|
||
}
|
||
|
||
msgBox.innerHTML =
|
||
(thinkStr ? `<div class="llm-think">${thinkStr}</div>` : "") +
|
||
`<span class="assistant-label">智能助手:</span><div class="llm-md">${html}</div>`;
|
||
|
||
bindAllCopyButtons(msgBox);
|
||
insertCopyFullAnswerBtn(msgBox, answerStr);
|
||
|
||
// 只在用户停留底部时才自动滚到底
|
||
if (resultContent && isUserAtBottom) {
|
||
resultContent.scrollTop = resultContent.scrollHeight;
|
||
}
|
||
|
||
// 收尾渲染引用 & 持久化历史
|
||
if (chunk.close === true) {
|
||
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";
|
||
msgBox.appendChild(sourceList);
|
||
await renderSources(sourceList, collectedSources, jumpMap);
|
||
insertCopyFullAnswerBtn(msgBox, answerStr);
|
||
}
|
||
// 历史追加 assistant 回答
|
||
appendLlmHistory({
|
||
role: "assistant",
|
||
text: answerStr,
|
||
sources: collectedSources,
|
||
jumpMap: jumpMap
|
||
});
|
||
// 自动滚动到底部
|
||
if (resultContent) resultContent.scrollTop = resultContent.scrollHeight;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
msgBox.innerHTML += `<span style='color:red;'>LLM异常:${e}</span>`;
|
||
console.error("LLM问答流式接口异常", e);
|
||
}
|
||
}
|
||
|
||
// 历史加载
|
||
async function renderLlmHistory(contentDiv) {
|
||
chrome.storage.local.get(["llmHistory"], (res) => {
|
||
const history = res.llmHistory || [];
|
||
contentDiv.innerHTML = "";
|
||
for (const item of history) {
|
||
if (item.role === 'user') {
|
||
const userItem = document.createElement("div");
|
||
userItem.className = "llm-msg-item llm-user";
|
||
userItem.innerHTML = `<span style="color:#2a63c8;font-weight:bold;">我:</span> ${item.text}`;
|
||
contentDiv.appendChild(userItem);
|
||
} else if (item.role === 'assistant') {
|
||
const msgBox = document.createElement("div");
|
||
msgBox.className = "llm-msg-item llm-assistant";
|
||
let html = "";
|
||
try {
|
||
html = window.marked ? wrapHtmlWithCopyBtn(marked.parse(item.text)) : item.text.replace(/\n/g, "<br>");
|
||
} catch (e) {
|
||
html = item.text.replace(/\n/g, "<br>");
|
||
}
|
||
msgBox.innerHTML = `<span class="assistant-label">智能助手:</span><div class="llm-md">${html}</div>`;
|
||
bindAllCopyButtons(msgBox);
|
||
insertCopyFullAnswerBtn(msgBox, item.text);
|
||
|
||
// 渲染参考资料
|
||
if (item.sources && item.sources.length) {
|
||
const sourceList = document.createElement("div");
|
||
sourceList.className = "llm-sources";
|
||
msgBox.appendChild(sourceList);
|
||
renderSources(sourceList, item.sources, item.jumpMap || {});
|
||
insertCopyFullAnswerBtn(msgBox, item.text);
|
||
}
|
||
contentDiv.appendChild(msgBox);
|
||
}
|
||
}
|
||
// 自动滚到底
|
||
contentDiv.scrollTop = contentDiv.scrollHeight;
|
||
});
|
||
}
|
||
|
||
// 对话历史追加
|
||
function appendLlmHistory(item) {
|
||
chrome.storage.local.get(["llmHistory"], (res) => {
|
||
const history = res.llmHistory || [];
|
||
history.push(item);
|
||
chrome.storage.local.set({ llmHistory: history });
|
||
});
|
||
}
|
||
|
||
// 清空历史按钮绑定
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
const clearBtn = document.getElementById("clearLlmHistoryBtn");
|
||
if (clearBtn) {
|
||
clearBtn.onclick = () => {
|
||
if (confirm("确定要清空智能助手历史记录吗?")) {
|
||
chrome.storage.local.set({ llmHistory: [] }, () => {
|
||
const contentDiv = document.getElementById("llmResultContent");
|
||
if (contentDiv) contentDiv.innerHTML = "";
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// textarea 自动高度适应
|
||
const input = document.getElementById("llmInput");
|
||
const sendBtn = document.getElementById("llmSendBtn");
|
||
|
||
if (input && input.tagName.toLowerCase() === 'textarea') {
|
||
function autoResizeTextarea(el) {
|
||
el.style.height = 'auto';
|
||
el.style.height = el.scrollHeight + 'px';
|
||
}
|
||
input.addEventListener("input", function() {
|
||
autoResizeTextarea(input);
|
||
});
|
||
autoResizeTextarea(input);
|
||
|
||
// 发送后清空并适应高度
|
||
sendBtn.addEventListener("click", () => {
|
||
setTimeout(() => {
|
||
input.value = "";
|
||
autoResizeTextarea(input);
|
||
}, 10); // 让你的业务代码先执行(如提取 input.value 后再清空)
|
||
});
|
||
|
||
// 支持Enter发送,Ctrl+Enter/Shift+Enter换行
|
||
input.addEventListener("keydown", function(e) {
|
||
if (e.key === "Enter" && !e.ctrlKey && !e.shiftKey) {
|
||
e.preventDefault();
|
||
sendBtn.click();
|
||
}
|
||
// 其他情况浏览器原生处理
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// 智能助手 tab 被激活时刷新历史
|
||
function tryRenderLlmHistoryOnTabSwitch() {
|
||
const tabAi = document.getElementById("tabAi");
|
||
tabAi && tabAi.addEventListener("click", () => {
|
||
const contentDiv = document.getElementById("llmResultContent");
|
||
renderLlmHistory(contentDiv);
|
||
});
|
||
}
|
||
tryRenderLlmHistoryOnTabSwitch();
|