优化markdown格式展示
This commit is contained in:
parent
5d879e490f
commit
d3c8841d01
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "渠道应用开发部知识库",
|
"name": "渠道应用开发部知识库",
|
||||||
"version": "1.5.4",
|
"version": "2.0.0",
|
||||||
"description": "通过关键词快速搜索内部文档",
|
"description": "通过关键词快速搜索内部文档",
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage"],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
|
|||||||
@ -55,12 +55,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- JS模块加载顺序非常重要,请保持此顺序 -->
|
<!-- JS模块加载顺序非常重要,请保持此顺序 -->
|
||||||
<script src="../scripts/utils.js"></script>
|
<script src="../scripts/utils.js"></script>
|
||||||
<script src="../scripts/auth.js"></script>
|
<script src="../scripts/auth.js"></script>
|
||||||
<script src="../scripts/llm.js"></script>
|
<script src="../scripts/llm.js"></script>
|
||||||
<script src="../scripts/popup.js"></script>
|
<script src="../scripts/popup.js"></script>
|
||||||
|
<!-- 新增 marked.js CDN -->
|
||||||
|
<script src="../scripts/markdown-min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
192
scripts/llm.js
192
scripts/llm.js
@ -3,7 +3,6 @@ const LLM_WORKSPACES_API = "http://llm.wisdompulse.cn/api/workspaces";
|
|||||||
const LLM_STREAM_API_BASE = "http://llm.wisdompulse.cn/api/workspace";
|
const LLM_STREAM_API_BASE = "http://llm.wisdompulse.cn/api/workspace";
|
||||||
const LLM_TOKEN_EXPIRE_MS = 23 * 60 * 60 * 1000;
|
const LLM_TOKEN_EXPIRE_MS = 23 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
|
||||||
async function resolveJumpLinks(titles) {
|
async function resolveJumpLinks(titles) {
|
||||||
if (!titles || titles.length === 0) return {};
|
if (!titles || titles.length === 0) return {};
|
||||||
|
|
||||||
@ -43,50 +42,6 @@ async function resolveJumpLinks(titles) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function renderSources(sourceList, sources) {
|
|
||||||
const titles = sources.map(s => s.title).filter(Boolean);
|
|
||||||
const jumpMap = await resolveJumpLinks(titles);
|
|
||||||
|
|
||||||
sourceList.style.marginTop = "16px";
|
|
||||||
sourceList.style.borderTop = "1px solid #dbe3f3";
|
|
||||||
sourceList.style.paddingTop = "10px";
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
for (const src of sources) {
|
|
||||||
const url = jumpMap[src.title];
|
|
||||||
const line = document.createElement("div");
|
|
||||||
line.style.marginTop = "4px";
|
|
||||||
|
|
||||||
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");
|
|
||||||
line.appendChild(link);
|
|
||||||
} else {
|
|
||||||
line.textContent = src.title || "未命名文档";
|
|
||||||
line.style.color = "#666";
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceList.appendChild(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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) => {
|
||||||
@ -152,6 +107,71 @@ 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;
|
||||||
|
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();
|
||||||
|
// 只复制紧邻 code 元素的内容
|
||||||
|
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) {
|
||||||
|
log("[复制代码] 成功,内容:", codeText);
|
||||||
|
} else {
|
||||||
|
console.error("[复制代码] 失败", error, "内容:", codeText);
|
||||||
|
alert("复制失败: " + (error ? error : "未知错误"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// fetchLLMAnswer,支持流式markdown、代码块复制按钮
|
||||||
async function fetchLLMAnswer(question, container, contentDiv) {
|
async function fetchLLMAnswer(question, container, contentDiv) {
|
||||||
if (!question || !question.trim()) return;
|
if (!question || !question.trim()) return;
|
||||||
|
|
||||||
@ -225,12 +245,23 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
if (inThink) {
|
if (inThink) {
|
||||||
thinkStr += content.replace(/\n/g, "<br>");
|
thinkStr += content.replace(/\n/g, "<br>");
|
||||||
} else {
|
} else {
|
||||||
answerStr += content.replace(/\n/g, "<br>");
|
answerStr += content; // 直接累计 markdown 原文
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染 markdown,代码块加复制按钮
|
||||||
|
let html = "";
|
||||||
|
try {
|
||||||
|
html = window.marked ? marked.parse(answerStr) : answerStr.replace(/\n/g, "<br>");
|
||||||
|
} catch (e) {
|
||||||
|
html = answerStr.replace(/\n/g, "<br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
msgBox.innerHTML =
|
msgBox.innerHTML =
|
||||||
(thinkStr ? `<div class="llm-think">${thinkStr}</div>` : "") +
|
(thinkStr ? `<div class="llm-think">${thinkStr}</div>` : "") +
|
||||||
`<span class="assistant-label">智能助手:</span>${answerStr}`;
|
`<span class="assistant-label">智能助手:</span><div class="llm-md">${wrapHtmlWithCopyBtn(html)}</div>`;
|
||||||
|
// 【关键】渲染后绑定复制事件
|
||||||
|
bindAllCopyButtons(msgBox);
|
||||||
|
|
||||||
contentDiv.scrollTop = contentDiv.scrollHeight;
|
contentDiv.scrollTop = contentDiv.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,3 +282,70 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
69
scripts/markdown-min.js
vendored
Normal file
69
scripts/markdown-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -156,3 +156,158 @@ body.fullscreen #tabContentAi.tab-content.active {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.llm-md {
|
||||||
|
font-size: 14.2px;
|
||||||
|
line-height: 1.72;
|
||||||
|
color: #222d3b;
|
||||||
|
background: #fcfcfe;
|
||||||
|
border-radius: 7px;
|
||||||
|
box-shadow: 0 1.5px 6px #e6ebfa33;
|
||||||
|
border: 1px solid #e7eaf5;
|
||||||
|
padding: 18px 18px 13px 18px;
|
||||||
|
margin: 9px 0 7px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 段落、标题、块状元素间距 */
|
||||||
|
.llm-md p,
|
||||||
|
.llm-md ul, .llm-md ol, .llm-md pre, .llm-md blockquote, .llm-md table {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.llm-md h1, .llm-md h2, .llm-md h3, .llm-md h4, .llm-md h5, .llm-md h6 {
|
||||||
|
margin-top: 1.2em;
|
||||||
|
margin-bottom: 0.6em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2560de;
|
||||||
|
line-height: 1.22;
|
||||||
|
}
|
||||||
|
.llm-md h1 { font-size: 1.22em; border-bottom: 2px solid #e7eaf5; padding-bottom: 3px; }
|
||||||
|
.llm-md h2 { font-size: 1.13em; border-bottom: 1px solid #eceff5; padding-bottom: 2px;}
|
||||||
|
.llm-md h3 { font-size: 1.07em; }
|
||||||
|
.llm-md h4, .llm-md h5, .llm-md h6 { font-size: 1em; }
|
||||||
|
|
||||||
|
.llm-md > * + * {
|
||||||
|
margin-top: 0.74em; /* 块元素之间舒适的间隔 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md ul, .llm-md ol {
|
||||||
|
padding-left: 1.45em;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.llm-md li {
|
||||||
|
margin-bottom: 0.16em;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md blockquote {
|
||||||
|
background: #f6f9fd;
|
||||||
|
border-left: 4px solid #a4c0f0;
|
||||||
|
padding: 8px 17px;
|
||||||
|
margin: 9px 0 11px 0;
|
||||||
|
color: #395fa9;
|
||||||
|
font-size: 13.6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md pre {
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #26324b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px 19px 15px 17px;
|
||||||
|
font-size: 13.5px;
|
||||||
|
margin: 8px 0 11px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
box-shadow: 0 2px 8px #e0eaf422;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #e3e8f2;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'monospace';
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md pre code {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md code {
|
||||||
|
background: #f7f6f4;
|
||||||
|
color: #a72543;
|
||||||
|
border-radius: 3.5px;
|
||||||
|
padding: 2.5px 6px;
|
||||||
|
font-size: 92%;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'monospace';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 复制按钮右上角美观悬浮 */
|
||||||
|
.llm-md .copy-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 14px;
|
||||||
|
background: #eef1f9;
|
||||||
|
color: #3661b2;
|
||||||
|
border: 1px solid #dde4ef;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 13px 2px 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.76;
|
||||||
|
z-index: 10;
|
||||||
|
transition: background 0.16s, opacity 0.16s;
|
||||||
|
box-shadow: 0 1px 3px #ccd4f522;
|
||||||
|
font-weight: 500;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 1.5;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md .copy-btn:hover {
|
||||||
|
background: #e1eaff;
|
||||||
|
opacity: 1;
|
||||||
|
color: #184d99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md pre:hover .copy-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 12px 0;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 5px #e3eaf5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.llm-md th, .llm-md td {
|
||||||
|
border: 1px solid #e5e9f2;
|
||||||
|
padding: 8px 13px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.llm-md th {
|
||||||
|
background: #f5f7fc;
|
||||||
|
color: #2c4e9c;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1.5px dashed #d1d7e6;
|
||||||
|
margin: 18px 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llm-md a {
|
||||||
|
color: #2560de;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
word-break: break-all;
|
||||||
|
transition: color 0.16s;
|
||||||
|
}
|
||||||
|
.llm-md a:hover {
|
||||||
|
color: #183b8e;
|
||||||
|
background: #e8f1ff;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user