feat: enhance Quick Notes 5.0 with API integration and UI improvements

- Implemented API communication for WeKnora, allowing users to authenticate and interact with knowledge bases.
- Added functionality for image uploads and previews in chat, enhancing multimodal interaction capabilities.
- Refactored the popup and sidepanel interfaces to improve user experience and accessibility, including new toolbar elements for mode selection.
- Introduced session management features, enabling users to manage chat sessions and knowledge base selections more effectively.
- Enhanced styling and layout for better responsiveness and visual appeal across various components.

These changes significantly improve the functionality and user experience of the Quick Notes extension, providing a more robust platform for managing web content and interactions.
This commit is contained in:
wizardchen
2026-03-13 16:43:56 +08:00
parent 7f8409652f
commit a9bc4bc102
7 changed files with 4145 additions and 266 deletions
@@ -1,5 +1,94 @@
// background.js — Service Worker
// 存储管理 + 消息路由 + 右键菜单
// 存储管理 + 消息路由 + 右键菜单 + API 通信
// === WeKnora API Helper ===
// 构建请求头:API Key 使用 X-API-Key header,Bearer token 使用 Authorization header
async function buildHeaders(config) {
var headers = {
'Content-Type': 'application/json',
'X-Request-ID': Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
};
if (config.apiKey) {
// API Key (sk- 开头) 使用 X-API-Key header 进行租户级认证
headers['X-API-Key'] = config.apiKey;
}
if (config.bearerToken) {
// Bearer token (通过用户名密码登录获取) 使用 Authorization header
headers['Authorization'] = 'Bearer ' + config.bearerToken;
}
return headers;
}
async function apiRequest(method, path, body, options) {
var config = await getConfigData();
if (!config || !config.baseUrl) {
return { success: false, error: '未配置服务地址,请先在设置中配置' };
}
var baseUrl = config.baseUrl.replace(/\/+$/, '');
var url = baseUrl + path;
var headers = await buildHeaders(config);
try {
var fetchOpts = { method: method, headers: headers };
if (body && method !== 'GET') {
fetchOpts.body = JSON.stringify(body);
}
if (options && options.signal) {
fetchOpts.signal = options.signal;
}
var resp = await fetch(url, fetchOpts);
if (!resp.ok) {
var errText = '';
try { var errJson = await resp.json(); errText = errJson.error?.message || errJson.message || resp.statusText; } catch (e) { errText = resp.statusText; }
return { success: false, error: errText, status: resp.status };
}
var data = await resp.json();
return data;
} catch (err) {
return { success: false, error: err.message || '网络请求失败' };
}
}
// SSE streaming chat request — returns ReadableStream
async function apiChatStream(path, body) {
var config = await getConfigData();
if (!config || !config.baseUrl) {
return { success: false, error: '未配置服务地址' };
}
var baseUrl = config.baseUrl.replace(/\/+$/, '');
var url = baseUrl + path;
var headers = await buildHeaders(config);
headers['Accept'] = 'text/event-stream';
try {
var resp = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body),
cache: 'no-store' // 避免浏览器缓存导致 SSE 流被缓冲
});
if (!resp.ok) {
var errText = '';
try { var errJson = await resp.json(); errText = errJson.error?.message || errJson.message || resp.statusText; } catch (e) { errText = resp.statusText; }
return { success: false, error: errText };
}
return { success: true, response: resp };
} catch (err) {
return { success: false, error: err.message || '网络请求失败' };
}
}
// 流式推送消息到 sidepanel / popup 等前端页面
function notifyStream(msg) {
chrome.runtime.sendMessage(msg).catch(function () {});
}
// Helper to get raw config data
async function getConfigData() {
var data = await chrome.storage.local.get('ka_config');
return data.ka_config || null;
}
// Session cache per tab for sidepanel chat
var chatSessions = {};
// === 右键菜单 ===
// 防止并发注册
@@ -58,9 +147,9 @@ async function updateContextMenuTitle() {
} else if (auth && auth.type === 'ka') {
askTitle = '使用知识助理提问';
}
try {
chrome.contextMenus.update('ka-ask-selection', { title: askTitle });
} catch (e) { /* 菜单还未创建时忽略 */ }
chrome.contextMenus.update('ka-ask-selection', { title: askTitle }, function () {
void chrome.runtime.lastError; // 菜单还未创建时静默忽略
});
}
chrome.contextMenus.onClicked.addListener(function (info, tab) {
@@ -182,6 +271,255 @@ async function handleMessage(msg, sender) {
} catch (err) {
return { success: false, error: err.message || '截图失败' };
}
// === WeKnora API 相关 ===
case 'VALIDATE_CONFIG':
// API Key 没有专门的验证端点,通过调用知识库列表接口来验证连通性和认证
return apiRequest('GET', '/knowledge-bases');
case 'LOGIN': {
// 用户名密码登录,获取 Bearer token
var loginPayload = msg.payload || {};
var loginResp = await apiRequest('POST', '/auth/login', {
email: loginPayload.email,
password: loginPayload.password
});
// 如果登录成功,将 token 存入 config
if (loginResp && loginResp.success && loginResp.token) {
var curConfig = await getConfigData() || {};
curConfig.bearerToken = loginResp.token;
curConfig.refreshToken = loginResp.refresh_token;
await chrome.storage.local.set({ ka_config: curConfig });
}
return loginResp;
}
case 'LIST_KNOWLEDGE_BASES':
// agent_id 参数仅用于共享智能体(跨租户),本地/内置智能体不传
var agentFilter = (msg.payload && msg.payload.sharedAgentId) ? '?agent_id=' + msg.payload.sharedAgentId : '';
return apiRequest('GET', '/knowledge-bases' + agentFilter);
case 'LIST_AGENTS':
return apiRequest('GET', '/agents');
case 'CREATE_KNOWLEDGE_BASE':
return apiRequest('POST', '/knowledge-bases', msg.payload);
case 'CREATE_SESSION':
return apiRequest('POST', '/sessions', msg.payload || {});
case 'LIST_SESSIONS':
var p = msg.payload || {};
return apiRequest('GET', '/sessions?page=' + (p.page || 1) + '&page_size=' + (p.page_size || 20));
case 'CLEAR_SESSION_MESSAGES': {
var sid = (msg.payload || {}).sessionId;
if (!sid) return { success: false, error: '缺少 sessionId' };
return apiRequest('DELETE', '/sessions/' + sid + '/messages');
}
case 'CHAT_QUERY': {
// 真正的知识库问答 — 使用 SSE 流式输出
var payload = msg.payload || {};
var query = payload.query;
if (!query) return { success: false, error: '请输入问题' };
// 获取或创建会话
var sessionId = payload.sessionId;
if (!sessionId) {
var sessionResp = await apiRequest('POST', '/sessions', {});
if (sessionResp && sessionResp.success && sessionResp.data) {
sessionId = sessionResp.data.id;
} else if (sessionResp && sessionResp.id) {
sessionId = sessionResp.id;
}
if (!sessionId) {
return { success: false, error: '创建会话失败: ' + (sessionResp.error || '未知错误') };
}
await chrome.storage.local.set({ ka_current_session: sessionId });
}
// 确定使用知识库问答还是智能体问答
var kbIds = payload.knowledgeBaseIds || [];
var agentId = payload.agentId || '';
var useAgent = payload.agentEnabled || false;
var chatPath = useAgent
? '/agent-chat/' + sessionId
: '/knowledge-chat/' + sessionId;
// 构建完整请求体,参考 CreateKnowledgeQARequest
var chatBody = { query: query };
if (kbIds.length > 0) {
chatBody.knowledge_base_ids = kbIds;
}
if (agentId) {
chatBody.agent_id = agentId;
}
if (useAgent) {
chatBody.agent_enabled = true;
}
if (payload.webSearchEnabled) {
chatBody.web_search_enabled = true;
}
if (payload.mentionedItems) {
chatBody.mentioned_items = payload.mentionedItems;
}
if (payload.images && payload.images.length > 0) {
chatBody.images = payload.images;
}
// 使用请求 ID 区分不同来源的流式推送
var chatRequestId = payload._requestId || (Date.now().toString(36) + Math.random().toString(36).slice(2, 6));
// SSE 流式请求
var streamResult = await apiChatStream(chatPath, chatBody);
if (!streamResult.success) {
return { success: false, error: streamResult.error };
}
// 读取 SSE 流,逐块推送到前端
try {
var reader = streamResult.response.body.getReader();
var decoder = new TextDecoder();
var fullText = '';
var buffer = '';
while (true) {
var readResult = await reader.read();
if (readResult.done) break;
buffer += decoder.decode(readResult.value, { stream: true });
// SSE 格式: "event: message\ndata: {json}\n\n"
// 按双换行分割完整事件块
var eventBlocks = buffer.split('\n\n');
buffer = eventBlocks.pop() || '';
for (var bi = 0; bi < eventBlocks.length; bi++) {
var block = eventBlocks[bi].trim();
if (!block) continue;
// 从事件块中提取 data 行
var dataLine = '';
var blockLines = block.split('\n');
for (var li = 0; li < blockLines.length; li++) {
var bline = blockLines[li];
if (bline.startsWith('data:')) {
dataLine = bline.substring(5).trim();
}
}
if (!dataLine || dataLine === '[DONE]') continue;
try {
var evt = JSON.parse(dataLine);
var responseType = evt.response_type || '';
// 根据 response_type 处理不同事件
if (responseType === 'answer') {
var chunk = evt.content || '';
if (chunk) {
fullText += chunk;
notifyStream({
type: 'CHAT_STREAM_CHUNK',
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'answer', content: chunk, done: !!evt.done }
});
}
} else if (responseType === 'thinking') {
notifyStream({
type: 'CHAT_STREAM_CHUNK',
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'thinking', content: evt.content || '', eventId: evt.data && evt.data.event_id }
});
} else if (responseType === 'tool_call') {
notifyStream({
type: 'CHAT_STREAM_CHUNK',
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'tool_call', content: evt.content || '', toolName: evt.data && evt.data.tool_name, eventId: evt.data && (evt.data.event_id || evt.data.tool_call_id) }
});
} else if (responseType === 'tool_result') {
notifyStream({
type: 'CHAT_STREAM_CHUNK',
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'tool_result', content: evt.content || '', toolName: evt.data && evt.data.tool_name, eventId: evt.data && (evt.data.event_id || evt.data.tool_call_id), success: evt.data && evt.data.success }
});
} else if (responseType === 'references') {
var kRefs = evt.knowledge_references;
if (Array.isArray(kRefs) && kRefs.length > 0) {
notifyStream({
type: 'CHAT_STREAM_CHUNK',
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'references', references: kRefs }
});
}
} else if (responseType === 'error') {
notifyStream({
type: 'CHAT_STREAM_CHUNK',
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'error', content: evt.content || '请求出错' }
});
} else if (responseType === 'complete') {
notifyStream({
type: 'CHAT_STREAM_CHUNK',
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'complete', done: true }
});
}
if (responseType === 'session_title') {
notifyStream({
type: 'CHAT_STREAM_CHUNK',
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'session_title', content: evt.content || '' }
});
}
// agent_query 等事件静默忽略
} catch (e) {
// 非 JSON data 行,忽略
}
}
}
return { success: true, data: fullText || '未获取到回复内容', sessionId: sessionId, requestId: chatRequestId };
} catch (streamErr) {
return { success: false, error: '读取回复流失败: ' + streamErr.message };
}
}
case 'SAVE_CLIP_TO_KB': {
// 保存剪藏内容到知识库(作为手动知识条目)
var pl = msg.payload || {};
if (!pl.kbId || !pl.content) return { success: false, error: '缺少知识库 ID 或内容' };
var contentWithMeta = pl.content;
if (pl.url) {
contentWithMeta = '> 来源: ' + pl.url + '\n\n' + pl.content;
}
return apiRequest('POST', '/knowledge-bases/' + pl.kbId + '/knowledge/manual', {
title: pl.title || '快速笔记剪藏',
content: contentWithMeta,
status: 'publish'
});
}
case 'FETCH_FILE': {
// 通过 background 代理带认证头请求文件(图片等),返回 data URL
var filePath = (msg.payload || {}).filePath;
if (!filePath) return { success: false, error: '缺少 filePath' };
var cfg = await getConfigData();
if (!cfg || !cfg.baseUrl) return { success: false, error: '未配置服务地址' };
var fileBaseUrl = cfg.baseUrl.replace(/\/+$/, '').replace(/\/api\/v\d+$/, '');
var fileUrl = fileBaseUrl + '/files?file_path=' + encodeURIComponent(filePath);
var fileHeaders = await buildHeaders(cfg);
try {
var fileResp = await fetch(fileUrl, { method: 'GET', headers: fileHeaders });
if (!fileResp.ok) return { success: false, error: 'HTTP ' + fileResp.status };
var blob = await fileResp.blob();
// 转为 data URL 以便跨上下文传递
var reader2 = new FileReader();
var dataUrl = await new Promise(function (resolve, reject) {
reader2.onload = function () { resolve(reader2.result); };
reader2.onerror = function () { reject(new Error('FileReader error')); };
reader2.readAsDataURL(blob);
});
return { success: true, dataUrl: dataUrl };
} catch (err) {
return { success: false, error: err.message || '文件请求失败' };
}
}
case 'GET_USER_INFO':
return apiRequest('GET', '/auth/me');
default:
return { success: false, error: '未知消息类型' };
}
@@ -385,22 +385,35 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
.note-save svg { width: 14px; height: 14px; }
/* ===== 知识问答 — 单行输入栏 ===== */
.chat-bar {
display: flex;
align-items: center;
/* 底部输入区域整体包裹 */
.chat-bar-wrapper {
background: var(--white);
border-radius: 50px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 1px rgba(0, 0, 0, 0.04);
padding: 5px 5px 5px 6px;
gap: 0;
margin-top: 4px;
overflow: visible;
position: relative;
transition: box-shadow 0.2s;
margin-top: 4px;
}
.chat-bar:focus-within {
.chat-bar-wrapper:focus-within {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1.5px rgba(7, 193, 96, 0.15);
}
/* 顶部工具栏:知识库 + 模式选择 */
.chat-bar-toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px 0;
}
.chat-bar {
display: flex;
align-items: center;
padding: 4px 5px 5px 8px;
gap: 4px;
position: relative;
}
/* 知识库选择按钮 */
.chat-bar-kb {
display: flex;
@@ -422,6 +435,21 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
.chat-bar-kb svg { width: 13px; height: 13px; flex-shrink: 0; }
.chat-bar-arrow { width: 10px !important; height: 10px !important; opacity: 0.5; }
/* 模式选择按钮 */
.chat-bar-mode {
display: flex; align-items: center; gap: 3px;
border: none; background: #f3f4f5; border-radius: 50px;
padding: 6px 8px 6px 10px; font-size: 11px; font-weight: 500;
color: var(--text2); cursor: pointer; white-space: nowrap;
transition: all 0.15s; flex-shrink: 0;
}
.chat-bar-mode:hover { background: #eaebed; color: var(--text); }
.chat-bar-mode svg { width: 13px; height: 13px; flex-shrink: 0; }
/* 模式下拉菜单 */
.popup-mode-menu { position: fixed; background: var(--white); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.06); padding: 6px 0; min-width: 160px; display: none; z-index: 100; }
.popup-mode-menu.show { display: block; }
/* 分隔竖线 */
.chat-bar-divider { width: 1px; height: 16px; background: #e5e5e5; margin: 0 8px; flex-shrink: 0; }
@@ -434,9 +462,13 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
font-family: inherit;
outline: none;
color: var(--text);
padding: 0;
padding: 6px 4px;
min-width: 0;
height: 30px;
max-height: 100px;
resize: none;
overflow-y: auto;
line-height: 18px;
}
.chat-bar-input::placeholder { color: #c0c0c0; }
@@ -491,28 +523,79 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
.kb-mode-item svg { width: 14px; height: 14px; }
/* 附件选项 */
.kb-attach-item { padding: 8px 14px; font-size: 13px; color: var(--text2); cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.15s; }
.kb-attach-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.kb-attach-item svg { width: 14px; height: 14px; }
/* 图片上传按钮 */
.popup-image-btn {
display: flex; align-items: center; justify-content: center;
border: none; background: none; color: #999; cursor: pointer;
width: 28px; height: 28px; padding: 0; flex-shrink: 0; border-radius: 6px; transition: all 0.15s;
}
.popup-image-btn:hover { color: var(--primary); background: rgba(7,193,96,0.06); }
.popup-image-btn svg { width: 16px; height: 16px; }
/* 图片预览条 */
.popup-image-previews {
display: flex; gap: 6px; padding: 4px 12px; overflow-x: auto; flex-shrink: 0;
}
.popup-img-item { position: relative; flex-shrink: 0; }
.popup-img-thumb { width: 40px; height: 40px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; }
.popup-img-remove {
position: absolute; top: -4px; right: -4px;
width: 14px; height: 14px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff;
font-size: 10px; line-height: 14px; text-align: center; cursor: pointer; border: none; padding: 0;
}
.popup-img-remove:hover { background: #e44; }
/* Settings Overlay */
.settings-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); display: none; z-index: 100; backdrop-filter: blur(2px); }
.settings-overlay.show { display: flex; align-items: center; justify-content: center; }
.settings-panel { background: var(--white); border-radius: 14px; padding: 18px 20px; width: 340px; box-shadow: 0 8px 32px rgba(0,0,0,0.15); }
.settings-handle { display: none; }
.settings-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; color: var(--text); }
.settings-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; color: var(--text); display: flex; align-items: center; justify-content: space-between; }
.settings-close { border: none; background: none; color: var(--text3); cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
.settings-close:hover { background: #f0f0f0; color: var(--text); }
.settings-close svg { width: 16px; height: 16px; }
.settings-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f2f2f2; }
.settings-item:last-child { border-bottom: none; }
.settings-label { font-size: 13px; color: var(--text3); }
.settings-value { font-size: 13px; color: var(--text); font-weight: 500; max-width: 200px; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.settings-logout { width: 100%; height: 40px; border: none; border-radius: 10px; background: #fff0f0; color: #e44; font-size: 13px; font-weight: 500; cursor: pointer; margin-top: 12px; transition: all 0.2s; }
.settings-logout:hover { background: #ffe5e5; }
.settings-item-clickable { cursor: pointer; border-radius: 6px; margin: 0 -6px; padding: 10px 6px !important; transition: background 0.15s; }
.settings-item-clickable:hover { background: rgba(7,193,96,0.06); }
.settings-item-clickable .settings-value { color: var(--primary); }
.clip-kb-card { padding: 8px 10px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 6px; cursor: pointer; transition: all 0.15s; }
.clip-kb-card:hover { border-color: var(--primary); background: rgba(7,193,96,0.03); }
.clip-kb-card.selected { border-color: var(--primary); background: rgba(7,193,96,0.06); }
.clip-kb-card-name { font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 1px; }
.clip-kb-card-desc { font-size: 10px; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clip-kb-card-meta { font-size: 10px; color: #ccc; margin-top: 2px; }
/* Toast */
.toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-80px); background: var(--text); color: #fff; padding: 10px 22px; border-radius: 22px; font-size: 13px; z-index: 200; transition: transform 0.3s ease; pointer-events: none; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.toast.show { transform: translateX(-50%) translateY(0); }
.toast.success { background: var(--primary); }
.toast.error { background: #e44; }
/* === Markdown 渲染样式 === */
.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; }
.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; }
.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; }
.md-h { margin: 8px 0 4px; line-height: 1.4; }
h1.md-h { font-size: 18px; font-weight: 700; }
h2.md-h { font-size: 16px; font-weight: 700; }
h3.md-h { font-size: 14px; font-weight: 600; }
h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; }
.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; }
.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; }
.md-list li { margin: 2px 0; }
.md-list .md-list { margin: 2px 0 2px 0; }
.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; }
.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; }
.md-p a:hover, .md-list a:hover { text-decoration: underline; }
.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; width: 100%; }
.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; }
.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; }
.md-table tr:hover td { background: rgba(7,193,96,0.02); }
</style>
</head>
<body>
@@ -634,14 +717,14 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
</button>
<div class="more-menu" id="more-menu">
<button class="more-menu-item" id="btn-home">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5L12 3l9 7.5"/><path d="M5 9.5V19a1 1 0 001 1h4v-5h4v5h4a1 1 0 001-1V9.5"/></svg>
首页
</button>
<button class="more-menu-item" id="btn-clips">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 016.5 2H19v16H6.5a2.5 2.5 0 000 5H19"/></svg>
笔记
</button>
<button class="more-menu-item" id="btn-sidebar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
对话面板
</button>
<button class="more-menu-item" id="btn-settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
设置
@@ -755,41 +838,49 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
</div>
</div>
<div class="chat-bar">
<div class="kb-dropdown" id="kb-dropdown">
<button class="chat-bar-kb" id="btn-kb-select">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
<span id="kb-name">全部</span>
<div class="popup-image-previews" id="popup-image-previews" style="display:none;"></div>
<div class="chat-bar-wrapper">
<div class="chat-bar-toolbar">
<div class="kb-dropdown" id="kb-dropdown">
<button class="chat-bar-kb" id="btn-kb-select">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
<span id="kb-name">全部</span>
<svg class="chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="kb-dropdown-menu" id="kb-menu">
<div class="kb-dropdown-item selected" data-kb="all">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
全部知识库
</div>
<div class="kb-dropdown-item" data-kb="quick-note">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
快速笔记
</div>
<div class="kb-dropdown-item" data-kb="web-collect">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
网页收藏
</div>
<div class="kb-dropdown-item" data-kb="product-doc">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
产品文档
</div>
<div class="kb-dropdown-item" data-kb="tech-spec">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
技术规范
</div>
</div>
</div>
<button class="chat-bar-mode" id="btn-mode-select">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
<span id="mode-name">快速回答</span>
<svg class="chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="kb-dropdown-menu" id="kb-menu">
<div class="kb-dropdown-item selected" data-kb="all">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
全部知识库
</div>
<div class="kb-dropdown-item" data-kb="quick-note">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
快速笔记
</div>
<div class="kb-dropdown-item" data-kb="web-collect">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
网页收藏
</div>
<div class="kb-dropdown-item" data-kb="product-doc">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
产品文档
</div>
<div class="kb-dropdown-item" data-kb="tech-spec">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
技术规范
</div>
<div class="kb-dropdown-divider"></div>
<div class="kb-dropdown-section">模式</div>
<div class="popup-mode-menu" id="popup-mode-menu">
<div class="kb-mode-item selected" data-mode="fast">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
@@ -805,19 +896,19 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
精确
</div>
<div class="kb-dropdown-divider"></div>
<div class="kb-attach-item" id="btn-attach">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
上传图片
</div>
</div>
</div>
<div class="chat-bar-divider"></div>
<input class="chat-bar-input" id="chat-input" type="text" placeholder="向知识库提问…" autocomplete="off">
<button class="chat-bar-send" id="btn-chat-send">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
</button>
<div class="chat-bar">
<button class="popup-image-btn" id="popup-image-btn" style="display:none;" title="上传图片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</button>
<textarea class="chat-bar-input" id="chat-input" placeholder="向知识库提问…" autocomplete="off" rows="1"></textarea>
<button class="chat-bar-send" id="btn-chat-send">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
</button>
</div>
</div>
<input type="file" id="popup-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
</div>
</div>
@@ -826,7 +917,12 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
<div class="settings-overlay" id="settings-overlay">
<div class="settings-panel">
<div class="settings-handle"></div>
<div class="settings-title">设置</div>
<div class="settings-title">
<span>设置</span>
<button class="settings-close" id="btn-settings-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div id="settings-content">
<div class="settings-item">
<span class="settings-label">账号类型</span>
@@ -844,6 +940,10 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
<span class="settings-label">API Key</span>
<span class="settings-value" id="set-key">-</span>
</div>
<div class="settings-item settings-item-clickable" id="set-clip-kb-row">
<span class="settings-label">剪藏知识库</span>
<span class="settings-value" id="set-clip-kb">未选择 ›</span>
</div>
<div class="settings-item">
<span class="settings-label">版本</span>
<span class="settings-value">v5.0.0</span>
@@ -853,6 +953,23 @@ body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC
</div>
</div>
<!-- 剪藏知识库选择面板 -->
<div class="settings-overlay" id="clip-kb-overlay">
<div class="settings-panel" style="max-height:300px;display:flex;flex-direction:column;width:300px;padding:14px 16px;">
<div class="settings-title" style="display:flex;align-items:center;justify-content:space-between;font-size:14px;margin-bottom:8px;">
<span>选择剪藏知识库</span>
<button id="clip-kb-close" style="border:none;background:none;font-size:18px;color:#999;cursor:pointer;padding:0 4px;">×</button>
</div>
<div class="clip-kb-desc" style="font-size:11px;color:#999;margin-bottom:8px;">剪藏的网页内容将存储到此知识库</div>
<div id="clip-kb-list" style="flex:1;overflow-y:auto;"></div>
<div id="clip-kb-empty" style="display:none;text-align:center;padding:16px 0;">
<div style="font-size:13px;color:#999;margin-bottom:8px;">暂无知识库</div>
<div style="font-size:12px;color:#bbb;margin-bottom:12px;">请前往 WeKnora 创建知识库后使用剪藏功能</div>
<button id="clip-kb-goto" style="height:32px;padding:0 16px;border:none;border-radius:16px;background:#07C160;color:#fff;font-size:12px;cursor:pointer;">前往创建</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
@@ -3,6 +3,8 @@
// === State ===
var currentUser = null; // { type: 'ka'|'wk', name, avatar, badge }
var clipKbId = ''; // 剪藏目标知识库 ID
var clipKbName = ''; // 剪藏目标知识库名称
// === DOM Helpers ===
function $(id) { return document.getElementById(id); }
@@ -167,8 +169,14 @@
msg.textContent = '正在验证…';
msg.className = 'ka-test-msg';
setTimeout(function () {
sendMsg({ type: 'SET_CONFIG', payload: { baseUrl: url, apiKey: key } }).then(function () {
// 先保存配置
sendMsg({ type: 'SET_CONFIG', payload: { baseUrl: url, apiKey: key } }).then(function () {
// 通过调用知识库列表来验证 API Key 和服务地址是否有效
return sendMsg({ type: 'VALIDATE_CONFIG' });
}).then(function (resp) {
// VALIDATE_CONFIG 现在实际调用 GET /knowledge-bases,成功说明连通且认证有效
if (resp && resp.success) {
// API Key 认证成功,不需要调用 /auth/me(那个需要 Bearer token)
sendMsg({ type: 'SET_AUTH', payload: { type: 'ka', name: '快速笔记用户', avatar: kaAvatarUrl } });
msg.textContent = '验证通过';
msg.className = 'ka-test-msg ok';
@@ -176,8 +184,11 @@
enterMain('ka', '快速笔记用户', kaAvatarUrl, '快速笔记 5.0');
toast('登录成功', 'success');
}, 500);
});
}, 800);
} else {
msg.textContent = '验证未通过,请检查服务地址和 API Key';
msg.className = 'ka-test-msg err';
}
});
});
// WeKnora 配置页
@@ -215,27 +226,58 @@
$('wk-test-msg').className = 'wk-test-result';
// 保存配置
sendMsg({ type: 'SET_CONFIG', payload: { baseUrl: url, apiKey: key } }).then(function () {
sendMsg({ type: 'SET_AUTH', payload: { type: 'wk', name: 'WeKnora 用户', avatar: '' } });
$('wk-test-msg').textContent = '配置已保存';
$('wk-test-msg').className = 'wk-test-result ok';
setTimeout(function () {
enterMain('wk', 'WeKnora 用户', '', 'WeKnora');
}, 500);
// 验证连接
return sendMsg({ type: 'VALIDATE_CONFIG' });
}).then(function (resp) {
if (resp && resp.success) {
// API Key 验证成功
sendMsg({ type: 'SET_AUTH', payload: { type: 'wk', name: 'WeKnora 用户', avatar: '' } });
$('wk-test-msg').textContent = '验证通过,配置已保存';
$('wk-test-msg').className = 'wk-test-result ok';
setTimeout(function () {
enterMain('wk', 'WeKnora 用户', '', 'WeKnora');
}, 500);
} else {
$('wk-test-msg').textContent = '验证未通过,请检查服务地址和 API Key';
$('wk-test-msg').className = 'wk-test-result err';
}
});
});
// === 更新用户信息显示 ===
function updateUserDisplay(type, name, avatarUrl, badge) {
if (avatarUrl) {
$('main-avatar-img').src = avatarUrl;
$('main-avatar-img').style.display = 'block';
$('main-letter').style.display = 'none';
} else {
$('main-avatar-img').style.display = 'none';
$('main-letter').style.display = 'flex';
$('main-letter').textContent = (name || 'U').charAt(0).toUpperCase();
$('main-letter').style.background = '#07C160';
}
$('main-name').textContent = name;
$('main-badge').textContent = badge;
$('main-badge').className = 'main-badge ' + (type === 'ka' ? 'badge-ka' : 'badge-wk');
}
// === 进入主界面 ===
function enterMain(type, name, avatarUrl, badge) {
currentUser = { type: type, name: name, avatar: avatarUrl, badge: badge };
// 统一使用字母头像:圆形绿色底 + 字母 S
$('main-avatar-img').style.display = 'none';
$('main-letter').style.display = 'flex';
$('main-letter').textContent = 'S';
$('main-letter').style.background = '#07C160';
// 设置名称和标签
$('main-name').textContent = name;
$('main-badge').textContent = badge;
$('main-badge').className = 'main-badge ' + (type === 'ka' ? 'badge-ka' : 'badge-wk');
updateUserDisplay(type, name, avatarUrl, badge);
// 从 API 获取真实用户信息
sendMsg({ type: 'GET_USER_INFO' }).then(function (resp) {
if (resp && resp.success && resp.data) {
var user = resp.data.user || resp.data;
var realName = user.username || user.name || name;
var realAvatar = user.avatar || avatarUrl;
currentUser.name = realName;
currentUser.avatar = realAvatar;
// 同步更新 auth 存储
sendMsg({ type: 'SET_AUTH', payload: { type: type, name: realName, avatar: realAvatar } });
updateUserDisplay(type, realName, realAvatar, badge);
}
});
// 设置面板数据
$('set-type').textContent = badge;
$('set-name').textContent = name;
@@ -264,6 +306,166 @@
}
goPage('pg-main');
loadLatestClip();
loadAgentsForDropdown();
}
// === 从 API 加载知识库列表到下拉菜单 ===
function loadKnowledgeBasesForDropdown() {
sendMsg({ type: 'LIST_KNOWLEDGE_BASES' }).then(function (resp) {
if (resp && resp.success && resp.data) {
var kbList = Array.isArray(resp.data) ? resp.data : (resp.data.items || []);
popupAllKbs = kbList;
filterAndRenderPopupKbs();
// 检查剪藏知识库选择
checkClipKbSelection(kbList);
}
});
}
var popupAllKbs = [];
var popupAgents = [];
// === 根据当前选中智能体配置过滤知识库 ===
function filterAndRenderPopupKbs() {
var filtered = popupAllKbs;
var agent = null;
for (var i = 0; i < popupAgents.length; i++) {
if (popupAgents[i].id === popupAgentId) { agent = popupAgents[i]; break; }
}
if (agent && agent.config) {
var mode = agent.config.kb_selection_mode || 'all';
if (mode === 'none') {
filtered = [];
} else if (mode === 'selected' && agent.config.knowledge_bases) {
var allowedIds = agent.config.knowledge_bases;
filtered = popupAllKbs.filter(function (kb) {
return allowedIds.indexOf(kb.id) !== -1;
});
}
}
renderKbDropdownItems(filtered);
}
// === 从 API 加载智能体列表到模式下拉 ===
function loadAgentsForDropdown() {
sendMsg({ type: 'LIST_AGENTS' }).then(function (resp) {
if (resp && resp.success && resp.data) {
popupAgents = Array.isArray(resp.data) ? resp.data : (resp.data.data || []);
if (popupAgents.length > 0) {
// 恢复持久化的模式选择
chrome.storage.local.get('ka_selected_agent', function (stored) {
if (!popupAgentId && stored && stored.ka_selected_agent && stored.ka_selected_agent.agentId) {
var saved = stored.ka_selected_agent;
for (var i = 0; i < popupAgents.length; i++) {
if (popupAgents[i].id === saved.agentId) {
popupAgentId = saved.agentId;
popupAgentEnabled = !!saved.agentEnabled;
break;
}
}
}
if (!popupAgentId) {
popupAgentId = popupAgents[0].id;
var isQA = popupAgents[0].id === 'builtin-quick-answer' || (popupAgents[0].config && popupAgents[0].config.agent_mode === 'quick-answer');
popupAgentEnabled = !isQA;
}
// 设置当前选中智能体的图片上传能力
for (var j = 0; j < popupAgents.length; j++) {
if (popupAgents[j].id === popupAgentId) {
popupAgentImageUpload = !!(popupAgents[j].config && popupAgents[j].config.image_upload_enabled);
break;
}
}
popupUpdateImageUI();
renderAgentModeItems(popupAgents);
// 加载全部知识库(前端根据智能体配置过滤)
loadKnowledgeBasesForDropdown();
});
} else {
loadKnowledgeBasesForDropdown();
}
} else {
loadKnowledgeBasesForDropdown();
}
});
}
function renderAgentModeItems(agentList) {
var modeMenu = $('popup-mode-menu');
if (!modeMenu) return;
// 移除旧的模式选项
var oldItems = modeMenu.querySelectorAll('.kb-mode-item');
oldItems.forEach(function (item) { item.remove(); });
agentList.forEach(function (agent, idx) {
var item = document.createElement('div');
var isQA = agent.id === 'builtin-quick-answer' || (agent.config && agent.config.agent_mode === 'quick-answer');
var isSelected = popupAgentId ? (agent.id === popupAgentId) : (idx === 0);
item.className = 'kb-mode-item' + (isSelected ? ' selected' : '');
item.setAttribute('data-agent-id', agent.id);
item.innerHTML = '<span class="kb-radio"></span> ' + (function (s) {
var d = document.createElement('div'); d.textContent = s; return d.innerHTML;
})(agent.name);
if (isSelected) {
$('mode-name').textContent = agent.name;
}
item.addEventListener('click', function (e) {
e.stopPropagation();
popupAgentId = agent.id;
popupAgentEnabled = !isQA;
modeMenu.querySelectorAll('.kb-mode-item').forEach(function (i) { i.classList.remove('selected'); });
item.classList.add('selected');
$('mode-name').textContent = agent.name;
modeMenu.classList.remove('show');
// 持久化模式选择,同步给 sidepanel / weknora
chrome.storage.local.set({ ka_selected_agent: { agentId: agent.id, agentEnabled: !isQA } });
// 更新图片上传按钮可见性
popupAgentImageUpload = !!(agent.config && agent.config.image_upload_enabled);
popupUpdateImageUI();
if (!popupAgentImageUpload && popupPendingImages.length > 0) popupClearImages();
// 切换智能体后根据配置过滤知识库
filterAndRenderPopupKbs();
});
modeMenu.appendChild(item);
});
}
function renderKbDropdownItems(kbList) {
var kbMenu = $('kb-menu');
if (!kbMenu) return;
// 移除旧的知识库选项(保留分隔线、模式等)
var oldItems = kbMenu.querySelectorAll('.kb-dropdown-item');
oldItems.forEach(function (item) { item.remove(); });
// 在第一个分隔线之前插入新选项
var firstDivider = kbMenu.querySelector('.kb-dropdown-divider');
// 插入 "全部" 选项
var allItem = createKbDropdownItem('all', '全部知识库', true);
kbMenu.insertBefore(allItem, firstDivider);
// 插入真实知识库
kbList.forEach(function (kb) {
var item = createKbDropdownItem(kb.id, kb.name, false);
kbMenu.insertBefore(item, firstDivider);
});
}
function createKbDropdownItem(kbId, name, isSelected) {
var div = document.createElement('div');
div.className = 'kb-dropdown-item' + (isSelected ? ' selected' : '');
div.setAttribute('data-kb', kbId);
div.innerHTML = '<span class="kb-radio"></span> ' + (function (s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
})(name);
div.addEventListener('click', function (e) {
e.stopPropagation();
selectedKb = kbId;
$('kb-name').textContent = name.length > 4 ? name.substring(0, 4) : name;
$('kb-menu').querySelectorAll('.kb-dropdown-item').forEach(function (i) { i.classList.remove('selected'); });
div.classList.add('selected');
$('kb-menu').classList.remove('show');
});
return div;
}
// === 加载并显示最新一条内容 ===
@@ -356,10 +558,114 @@
var chatInput = $('chat-input');
var sendBtn = $('btn-chat-send');
var selectedKb = 'all';
var selectedMode = 'fast';
var popupAgentId = '';
var popupAgentEnabled = false;
var popupAgentImageUpload = false;
// 图片上传
var popupPendingImages = [];
var POPUP_MAX_IMAGES = 5;
var POPUP_ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
var POPUP_MAX_IMAGE_SIZE = 10 * 1024 * 1024;
var popupImageInput = $('popup-image-input');
var popupImageBtn = $('popup-image-btn');
var popupImagePreviews = $('popup-image-previews');
function popupAddImages(files) {
if (!popupAgentImageUpload) return;
for (var i = 0; i < files.length; i++) {
if (popupPendingImages.length >= POPUP_MAX_IMAGES) { break; }
var f = files[i];
if (POPUP_ALLOWED_TYPES.indexOf(f.type) === -1 || f.size > POPUP_MAX_IMAGE_SIZE) continue;
popupPendingImages.push({ file: f, preview: URL.createObjectURL(f) });
}
popupRenderPreviews();
}
function popupRemoveImage(idx) {
if (idx >= 0 && idx < popupPendingImages.length) {
URL.revokeObjectURL(popupPendingImages[idx].preview);
popupPendingImages.splice(idx, 1);
}
popupRenderPreviews();
}
function popupClearImages() {
popupPendingImages.forEach(function (img) { URL.revokeObjectURL(img.preview); });
popupPendingImages = [];
popupRenderPreviews();
}
function popupRenderPreviews() {
if (!popupImagePreviews) return;
if (popupPendingImages.length === 0) {
popupImagePreviews.style.display = 'none';
popupImagePreviews.innerHTML = '';
return;
}
popupImagePreviews.style.display = 'flex';
var html = '';
for (var i = 0; i < popupPendingImages.length; i++) {
html += '<div class="popup-img-item" data-idx="' + i + '">'
+ '<img class="popup-img-thumb" src="' + popupPendingImages[i].preview + '">'
+ '<span class="popup-img-remove">&times;</span></div>';
}
popupImagePreviews.innerHTML = html;
popupImagePreviews.querySelectorAll('.popup-img-remove').forEach(function (btn) {
btn.addEventListener('click', function () {
popupRemoveImage(parseInt(btn.parentElement.getAttribute('data-idx')));
});
});
}
function popupUpdateImageUI() {
if (popupImageBtn) popupImageBtn.style.display = popupAgentImageUpload ? '' : 'none';
if (!popupAgentImageUpload && popupPendingImages.length > 0) popupClearImages();
}
function popupFileToBase64(file) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function () { resolve(reader.result); };
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
if (popupImageInput) {
popupImageInput.addEventListener('change', function () {
if (popupImageInput.files) popupAddImages(Array.from(popupImageInput.files));
popupImageInput.value = '';
});
}
if (popupImageBtn) {
popupImageBtn.addEventListener('click', function () {
if (popupImageInput) popupImageInput.click();
});
}
chatInput.addEventListener('paste', function (e) {
if (!popupAgentImageUpload) return;
var items = e.clipboardData && e.clipboardData.items;
if (!items) return;
var imageFiles = [];
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image/') === 0) {
var f = items[i].getAsFile();
if (f) imageFiles.push(f);
}
}
if (imageFiles.length > 0) {
e.preventDefault();
popupAddImages(imageFiles);
}
});
chatInput.addEventListener('input', function () {
sendBtn.classList.toggle('active', chatInput.value.trim().length > 0);
// 自动调整高度
chatInput.style.height = 'auto';
chatInput.style.height = Math.min(chatInput.scrollHeight, 100) + 'px';
});
chatInput.addEventListener('keydown', function (e) {
@@ -367,6 +673,7 @@
e.preventDefault();
sendChat();
}
// Shift+Enter 默认行为即换行,无需额外处理
});
sendBtn.addEventListener('click', sendChat);
@@ -374,32 +681,48 @@
function sendChat() {
var text = chatInput.value.trim();
if (!text) return;
var queryData = { query: text, user: currentUser, kb: selectedKb, mode: selectedMode, ts: Date.now() };
var queryData = { query: text, user: currentUser, kb: selectedKb, ts: Date.now(), agentId: popupAgentId, agentEnabled: popupAgentEnabled };
// 如果选中了具体知识库,传递知识库 ID
if (selectedKb && selectedKb !== 'all') {
queryData.knowledgeBaseIds = [selectedKb];
}
if (chrome && chrome.storage) {
chrome.storage.local.set({ ka_pending_query: queryData });
function doPopupSend(data) {
if (chrome && chrome.storage) {
chrome.storage.local.set({ ka_pending_query: data });
}
if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
chrome.runtime.sendMessage({ type: 'CHAT_QUERY', payload: data }).catch(function () {});
}
if (chrome && chrome.tabs) {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs && tabs[0]) {
chrome.sidePanel.open({ tabId: tabs[0].id }).catch(function () {});
}
window.close();
});
}
}
if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
chrome.runtime.sendMessage({ type: 'CHAT_QUERY', payload: queryData }).catch(function () {});
}
if (chrome && chrome.tabs) {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs && tabs[0]) {
chrome.sidePanel.open({ tabId: tabs[0].id }).catch(function () {});
}
window.close();
if (popupPendingImages.length > 0) {
var promises = popupPendingImages.map(function (img) { return popupFileToBase64(img.file); });
Promise.all(promises).then(function (dataURIs) {
queryData.images = dataURIs.map(function (d) { return { data: d }; });
popupClearImages();
doPopupSend(queryData);
});
} else {
doPopupSend(queryData);
}
}
// === 知识库下拉(合并模式选项) ===
// === 知识库下拉 ===
var kbMenu = $('kb-menu');
var kbBtn = $('btn-kb-select');
function positionKbMenu() {
var rect = kbBtn.getBoundingClientRect();
var menuH = kbMenu.offsetHeight;
// 优先向上弹出;空间不足则向下
var top = rect.top - menuH - 6;
if (top < 4) top = rect.bottom + 6;
kbMenu.style.left = rect.left + 'px';
@@ -408,10 +731,10 @@
kbBtn.addEventListener('click', function (e) {
e.stopPropagation();
popupModeMenu.classList.remove('show');
var willShow = !kbMenu.classList.contains('show');
kbMenu.classList.toggle('show');
if (willShow) {
// 先显示再计算高度
requestAnimationFrame(positionKbMenu);
}
});
@@ -427,20 +750,38 @@
kbMenu.classList.remove('show');
});
});
// 模式选择(合并在知识库菜单内)
kbMenu.querySelectorAll('.kb-mode-item').forEach(function (item) {
item.addEventListener('click', function (e) {
e.stopPropagation();
selectedMode = item.getAttribute('data-mode');
kbMenu.querySelectorAll('.kb-mode-item').forEach(function (i) { i.classList.remove('selected'); });
item.classList.add('selected');
kbMenu.classList.remove('show');
});
// === 模式下拉(独立) ===
var popupModeMenu = $('popup-mode-menu');
var modeBtn = $('btn-mode-select');
function positionModeMenu() {
var rect = modeBtn.getBoundingClientRect();
var menuH = popupModeMenu.offsetHeight;
var top = rect.top - menuH - 6;
if (top < 4) top = rect.bottom + 6;
popupModeMenu.style.left = rect.left + 'px';
popupModeMenu.style.top = top + 'px';
}
modeBtn.addEventListener('click', function (e) {
e.stopPropagation();
kbMenu.classList.remove('show');
var willShow = !popupModeMenu.classList.contains('show');
popupModeMenu.classList.toggle('show');
if (willShow) {
requestAnimationFrame(positionModeMenu);
}
});
popupModeMenu.addEventListener('click', function (e) {
e.stopPropagation();
});
// 点击其他地方关闭下拉
document.addEventListener('click', function () {
kbMenu.classList.remove('show');
popupModeMenu.classList.remove('show');
if (moreMenu) moreMenu.classList.remove('show');
});
@@ -586,61 +927,186 @@
function renderMarkdown(text) {
if (!text) return '';
var html = text;
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
return '<pre><code>' + escapeHtml(code.replace(/\n$/, '')) + '</code></pre>';
// 1. 提取代码块,替换为占位符
var codeBlocks = [];
var processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
var idx = codeBlocks.length;
codeBlocks.push('<pre class="md-pre"><code>' + escapeHtml(code.replace(/\n$/, '')) + '</code></pre>');
return '\x00CODEBLOCK' + idx + '\x00';
});
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/((?:\|.+\|\n)+)/g, function (tableBlock) {
// 2. 提取行内代码
var inlineCodes = [];
processed = processed.replace(/`([^`]+)`/g, function (_, code) {
var idx = inlineCodes.length;
inlineCodes.push('<code class="md-code">' + escapeHtml(code) + '</code>');
return '\x00INLINE' + idx + '\x00';
});
// 3. 表格处理 — 提取为占位符
var tables = [];
processed = processed.replace(/((?:\|.+\|\n)+)/g, function (tableBlock) {
var rows = tableBlock.trim().split('\n');
if (rows.length < 2) return tableBlock;
var out = '<table>';
var out = '<table class="md-table">';
rows.forEach(function (row, i) {
if (/^\|[\s\-:|]+\|$/.test(row)) return;
var tag = i === 0 ? 'th' : 'td';
var cells = row.split('|').filter(function (c, ci, arr) { return ci > 0 && ci < arr.length - 1; });
out += '<tr>';
cells.forEach(function (cell) { out += '<' + tag + '>' + cell.trim() + '</' + tag + '>'; });
cells.forEach(function (cell) {
out += '<' + tag + '>' + cell.trim() + '</' + tag + '>';
});
out += '</tr>';
});
out += '</table>';
return out;
var idx = tables.length;
tables.push(out);
return '\n\x00TABLE' + idx + '\x00\n';
});
var lines = html.split('\n');
var result = [];
var inList = false;
var listType = '';
for (var i = 0; i < lines.length; i++) {
// 4. 逐行解析为块元素
var lines = processed.split('\n');
var html = '';
var i = 0;
while (i < lines.length) {
var line = lines[i];
if (line.indexOf('<pre>') !== -1 || line.indexOf('<table>') !== -1) {
if (inList) { result.push('</' + listType + '>'); inList = false; }
result.push(line); continue;
// 代码块占位符
var cbMatch = line.match(/^\x00CODEBLOCK(\d+)\x00$/);
if (cbMatch) {
html += codeBlocks[parseInt(cbMatch[1])];
i++; continue;
}
if (/^####\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h4>' + RegExp.$1 + '</h4>'); continue; }
if (/^###\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h3>' + RegExp.$1 + '</h3>'); continue; }
if (/^##\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h2>' + RegExp.$1 + '</h2>'); continue; }
if (/^#\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h1>' + RegExp.$1 + '</h1>'); continue; }
if (/^---+$/.test(line.trim())) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<hr>'); continue; }
if (/^>\s*(.*)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<blockquote>' + RegExp.$1 + '</blockquote>'); continue; }
if (/^[\-\*]\s+(.+)/.test(line)) {
if (!inList || listType !== 'ul') { if (inList) result.push('</' + listType + '>'); result.push('<ul>'); inList = true; listType = 'ul'; }
result.push('<li>' + RegExp.$1 + '</li>'); continue;
// 表格占位符
var tbMatch = line.match(/^\x00TABLE(\d+)\x00$/);
if (tbMatch) {
html += tables[parseInt(tbMatch[1])];
i++; continue;
}
if (/^\d+\.\s+(.+)/.test(line)) {
if (!inList || listType !== 'ol') { if (inList) result.push('</' + listType + '>'); result.push('<ol>'); inList = true; listType = 'ol'; }
result.push('<li>' + RegExp.$1 + '</li>'); continue;
// 标题
if (/^(#{1,6})\s+(.+)/.test(line)) {
var level = RegExp.$1.length;
html += '<h' + level + ' class="md-h">' + inlineFormat(RegExp.$2) + '</h' + level + '>';
i++; continue;
}
if (inList) { result.push('</' + listType + '>'); inList = false; }
if (line.trim() === '') continue;
result.push('<p>' + line + '</p>');
// 引用块
if (/^>\s*(.*)/.test(line)) {
var bqLines = [];
while (i < lines.length && /^>\s*(.*)/.test(lines[i])) {
bqLines.push(RegExp.$1);
i++;
}
html += '<blockquote class="md-bq">' + inlineFormat(bqLines.join('<br>')) + '</blockquote>';
continue;
}
// 无序列表
if (/^(\s*)[*\-]\s+(.+)/.test(line)) {
html += parseList(lines, i, 'ul');
while (i < lines.length && /^(\s*)[*\-]\s+/.test(lines[i])) i++;
continue;
}
// 有序列表
if (/^(\s*)\d+\.\s+(.+)/.test(line)) {
html += parseList(lines, i, 'ol');
while (i < lines.length && /^(\s*)\d+\.\s+/.test(lines[i])) i++;
continue;
}
// 水平线
if (/^[-*_]{3,}\s*$/.test(line.trim())) {
html += '<hr>';
i++; continue;
}
// 空行
if (line.trim() === '') {
i++; continue;
}
// 普通段落
var pLines = [];
while (i < lines.length) {
var pl = lines[i];
if (pl.trim() === '' || /^#{1,6}\s/.test(pl) || /^>\s/.test(pl) ||
/^(\s*)[*\-]\s+/.test(pl) || /^(\s*)\d+\.\s+/.test(pl) ||
/^\x00CODEBLOCK/.test(pl) || /^\x00TABLE/.test(pl) ||
/^[-*_]{3,}\s*$/.test(pl.trim())) break;
pLines.push(pl);
i++;
}
html += '<p class="md-p">' + inlineFormat(pLines.join('<br>')) + '</p>';
}
if (inList) result.push('</' + listType + '>');
html = result.join('\n');
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// 5. 恢复行内代码占位符
html = html.replace(/\x00INLINE(\d+)\x00/g, function (_, idx) {
return inlineCodes[parseInt(idx)];
});
return html;
// --- 列表解析(支持嵌套)---
function parseList(allLines, startIdx, defaultType) {
var items = [];
var j = startIdx;
var baseIndent = -1;
var listTag = defaultType;
var re = listTag === 'ul' ? /^(\s*)[*\-]\s+(.+)/ : /^(\s*)\d+\.\s+(.+)/;
while (j < allLines.length) {
var m = allLines[j].match(re);
if (!m) break;
var indent = m[1].length;
if (baseIndent < 0) baseIndent = indent;
if (indent > baseIndent) {
var subStart = j;
while (j < allLines.length) {
var sm = allLines[j].match(re);
if (!sm || sm[1].length < indent) break;
j++;
}
var subHtml = parseList(allLines, subStart, listTag);
if (items.length > 0) {
items[items.length - 1] += subHtml;
}
continue;
}
if (indent < baseIndent) break;
items.push(inlineFormat(m[2]));
j++;
}
var out = '<' + listTag + ' class="md-list">';
for (var k = 0; k < items.length; k++) {
out += '<li>' + items[k] + '</li>';
}
out += '</' + listTag + '>';
return out;
}
// --- 行内格式化 ---
function inlineFormat(s) {
if (!s) return '';
// 图片(必须在链接之前)
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:6px;margin:4px 0;">');
// 链接
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
s = s.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
s = s.replace(/(?<!\*)\*([^\s*][^*]*?)\*(?!\*)/g, '<em>$1</em>');
s = s.replace(/~~(.+?)~~/g, '<del>$1</del>');
s = s.replace(/\x00INLINE(\d+)\x00/g, function (_, idx) {
return inlineCodes[parseInt(idx)];
});
return s;
}
}
// === Markdown 速记 ===
@@ -795,31 +1261,160 @@
}
});
// === 主页按钮 ===
$('btn-home').addEventListener('click', function () {
moreMenu.classList.remove('show');
// 打开 WeKnora 管理页面,弹窗保留
if (chrome && chrome.tabs) {
chrome.tabs.create({ url: chrome.runtime.getURL('weknora.html') });
$('btn-settings-close').addEventListener('click', function () {
$('settings-overlay').classList.remove('show');
});
// === 剪藏知识库选择 ===
var clipKbOverlay = $('clip-kb-overlay');
// 检查是否需要引导用户选择剪藏知识库
function checkClipKbSelection(kbList) {
chrome.storage.local.get(['clipKbId', 'clipKbName'], function (data) {
clipKbId = data.clipKbId || '';
clipKbName = data.clipKbName || '';
// 验证保存的 KB 是否仍存在
if (clipKbId) {
var found = kbList.some(function (kb) { return kb.id === clipKbId; });
if (!found) {
clipKbId = '';
clipKbName = '';
chrome.storage.local.remove(['clipKbId', 'clipKbName']);
}
}
// 更新设置面板显示
updateClipKbDisplay();
// 首次使用 — 引导选择
if (!clipKbId) {
showClipKbSelector(kbList);
}
});
}
function updateClipKbDisplay() {
var el = $('set-clip-kb');
if (el) {
el.textContent = clipKbName ? (clipKbName + ' ›') : '未选择 ›';
}
}
function showClipKbSelector(kbList) {
var list = kbList || popupAllKbs;
var listEl = $('clip-kb-list');
var emptyEl = $('clip-kb-empty');
if (!list || list.length === 0) {
listEl.style.display = 'none';
emptyEl.style.display = 'block';
} else {
goPage('pg-login');
listEl.style.display = 'block';
emptyEl.style.display = 'none';
var html = '';
for (var i = 0; i < list.length; i++) {
var kb = list[i];
var selected = kb.id === clipKbId ? ' selected' : '';
html += '<div class="clip-kb-card' + selected + '" data-kb-id="' + kb.id + '" data-kb-name="' + escapeHtml(kb.name) + '">'
+ '<div class="clip-kb-card-name">' + escapeHtml(kb.name) + '</div>'
+ '<div class="clip-kb-card-desc">' + escapeHtml(kb.description || '暂无描述') + '</div>'
+ '<div class="clip-kb-card-meta">' + (kb.document_count || 0) + ' 文档 · ' + (kb.chunk_count || 0) + ' 分段</div>'
+ '</div>';
}
listEl.innerHTML = html;
// 点击选择
listEl.querySelectorAll('.clip-kb-card').forEach(function (card) {
card.addEventListener('click', function () {
clipKbId = card.getAttribute('data-kb-id');
clipKbName = card.getAttribute('data-kb-name');
chrome.storage.local.set({ clipKbId: clipKbId, clipKbName: clipKbName });
updateClipKbDisplay();
clipKbOverlay.classList.remove('show');
toast('已选择知识库: ' + clipKbName, 'success');
});
});
}
clipKbOverlay.classList.add('show');
}
// 设置面板中点击"剪藏知识库"
$('set-clip-kb-row').addEventListener('click', function () {
$('settings-overlay').classList.remove('show');
showClipKbSelector();
});
// 关闭按钮
$('clip-kb-close').addEventListener('click', function () {
clipKbOverlay.classList.remove('show');
});
// 背景点击关闭
clipKbOverlay.addEventListener('click', function (e) {
if (e.target === clipKbOverlay) {
clipKbOverlay.classList.remove('show');
}
});
// === 查看收藏按钮 ===
$('btn-clips').addEventListener('click', function () {
// "前往创建"按钮 — 打开 WeKnora 主站
$('clip-kb-goto').addEventListener('click', function () {
clipKbOverlay.classList.remove('show');
sendMsg({ type: 'GET_CONFIG' }).then(function (resp) {
if (resp && resp.success && resp.data && resp.data.baseUrl) {
var baseUrl = resp.data.baseUrl.replace(/\/api\/v1\/?$/, '');
if (chrome && chrome.tabs) {
chrome.tabs.create({ url: baseUrl });
}
} else {
toast('请先配置服务地址', 'error');
}
});
});
// === 打开对话面板(sidebar)===
$('btn-sidebar').addEventListener('click', function () {
moreMenu.classList.remove('show');
if (chrome && chrome.tabs) {
chrome.tabs.create({ url: chrome.runtime.getURL('clips.html') });
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs && tabs[0]) {
chrome.sidePanel.open({ tabId: tabs[0].id }).catch(function () {});
}
window.close();
});
}
});
// === 打开笔记/知识库页面 ===
function openNotesPage() {
if (!currentUser || !chrome || !chrome.tabs) return;
if (currentUser.type === 'wk') {
// 自部署 WeKnora — 用配置的 baseUrl
sendMsg({ type: 'GET_CONFIG' }).then(function (resp) {
if (resp && resp.success && resp.data && resp.data.baseUrl) {
var url = resp.data.baseUrl.replace(/\/api\/v1\/?$/, '');
chrome.tabs.create({ url: url });
} else {
toast('请先配置服务地址', 'error');
}
});
} else {
// 知识助手官网用户
chrome.tabs.create({ url: 'https://weknora.weixin.qq.com' });
}
}
// === 查看笔记按钮 ===
$('btn-clips').addEventListener('click', function () {
moreMenu.classList.remove('show');
openNotesPage();
});
// === 最新内容卡片 — 查看全部 ===
$('btn-latest-more').addEventListener('click', function (e) {
e.stopPropagation();
if (chrome && chrome.tabs) {
chrome.tabs.create({ url: chrome.runtime.getURL('clips.html') });
}
openNotesPage();
});
// 点击最新内容卡片本身也打开列表页;双击翻转进入速记编辑
@@ -837,9 +1432,7 @@
// 单击:延迟执行,等看有没有双击
clickTimer = setTimeout(function () {
clickTimer = null;
if (chrome && chrome.tabs) {
chrome.tabs.create({ url: chrome.runtime.getURL('clips.html') });
}
openNotesPage();
}, 250);
}
});
@@ -23,7 +23,10 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
}
.header-icon { width: 30px; height: 30px; background: var(--primary); border-radius: 10px; display: flex; align-items: center; justify-content: center; }
.header-icon svg { width: 16px; height: 16px; color: #fff; }
.header h1 { font-size: 15px; font-weight: 600; flex: 1; }
.header h1 { font-size: 15px; font-weight: 600; flex: 1; min-width: 0; }
.header-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
.header-session-title { font-size: 11px; font-weight: 400; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: none; margin-top: 1px; }
.header-session-title.show { display: block; }
.header-btn { width: 32px; height: 32px; border: none; background: #f5f6f7; border-radius: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); transition: all 0.2s; }
.header-btn:hover { background: #eee; color: var(--text); }
.header-btn svg { width: 16px; height: 16px; }
@@ -38,6 +41,8 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 14px;
line-height: 1.6;
word-wrap: break-word;
overflow: hidden;
flex-shrink: 0;
}
.msg-user {
align-self: flex-end;
@@ -100,27 +105,41 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
}
.quick-btn:hover { background: rgba(7,193,96,0.08); color: var(--primary); box-shadow: 0 2px 8px rgba(7,193,96,0.12); }
/* ===== Input Area — 单行胶囊输入栏 ===== */
/* ===== Input Area ===== */
.input-area {
padding: 10px 14px 14px;
flex-shrink: 0;
background: transparent;
}
.sp-chat-bar {
display: flex;
align-items: center;
/* 整体包裹卡片 */
.sp-chat-bar-wrapper {
background: var(--white);
border-radius: 50px;
border-radius: 16px;
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 20px rgba(0,0,0,0.10);
padding: 5px 5px 5px 6px;
gap: 0;
overflow: visible;
position: relative;
transition: box-shadow 0.2s;
}
.sp-chat-bar:focus-within {
.sp-chat-bar-wrapper:focus-within {
box-shadow: 0 4px 24px rgba(0,0,0,0.10), 0 0 0 1.5px rgba(7,193,96,0.18);
}
/* 顶部工具栏:知识库 + 模式 */
.sp-chat-bar-toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px 0;
}
.sp-chat-bar {
display: flex;
align-items: center;
padding: 4px 5px 5px 8px;
gap: 4px;
position: relative;
}
/* 知识库选择按钮 */
.sp-chat-bar-kb {
display: flex;
@@ -142,6 +161,25 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
.sp-chat-bar-kb svg { width: 13px; height: 13px; flex-shrink: 0; }
.sp-chat-bar-arrow { width: 10px !important; height: 10px !important; opacity: 0.5; }
/* 模式选择按钮 */
.sp-chat-bar-mode {
display: flex; align-items: center; gap: 3px;
border: none; background: #f3f4f5; border-radius: 50px;
padding: 6px 8px 6px 10px; font-size: 11px; font-weight: 500;
color: var(--text2); cursor: pointer; white-space: nowrap;
transition: all 0.15s; flex-shrink: 0;
}
.sp-chat-bar-mode:hover { background: #eaebed; color: var(--text); }
.sp-chat-bar-mode svg { width: 13px; height: 13px; flex-shrink: 0; }
/* 模式下拉菜单 */
.sp-mode-dropdown-menu {
position: fixed; background: var(--white); border-radius: 14px;
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.14);
padding: 6px 0; min-width: 160px; display: none; z-index: 100;
}
.sp-mode-dropdown-menu.show { display: block; }
/* 分隔竖线 */
.sp-chat-bar-divider { width: 1px; height: 16px; background: #e5e5e5; margin: 0 8px; flex-shrink: 0; }
@@ -154,9 +192,13 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
font-family: inherit;
outline: none;
color: var(--text);
padding: 0;
padding: 6px 4px;
min-width: 0;
height: 30px;
max-height: 120px;
resize: none;
overflow-y: auto;
line-height: 18px;
}
.sp-chat-bar-input::placeholder { color: #c0c0c0; }
@@ -179,6 +221,33 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
.sp-chat-bar-send.active { background: var(--primary); color: #fff; box-shadow: 0 2px 8px rgba(7,193,96,0.25); }
.sp-chat-bar-send.active:hover { background: var(--primary-dark); }
/* 图片上传按钮 */
.sp-chat-bar-img {
display: flex; align-items: center; justify-content: center;
border: none; background: none; color: #999; cursor: pointer;
width: 28px; height: 28px; padding: 0; flex-shrink: 0; border-radius: 6px; transition: all 0.15s;
}
.sp-chat-bar-img:hover { color: var(--primary); background: rgba(7,193,96,0.06); }
.sp-chat-bar-img svg { width: 16px; height: 16px; }
/* 图片预览条 */
.sp-image-previews {
display: flex; gap: 6px; padding: 6px 12px 0; overflow-x: auto; flex-shrink: 0;
}
.sp-img-item { position: relative; flex-shrink: 0; }
.sp-img-thumb { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; }
.sp-img-remove {
position: absolute; top: -4px; right: -4px;
width: 16px; height: 16px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff;
font-size: 12px; line-height: 16px; text-align: center; cursor: pointer; border: none; padding: 0;
}
.sp-img-remove:hover { background: #e44; }
/* 用户消息中的图片 */
.msg-user-images { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.msg-user-img { width: 60px; height: 60px; border-radius: 8px; object-fit: cover; border: 1px solid rgba(255,255,255,0.3); cursor: pointer; }
.msg-user-img:hover { opacity: 0.85; }
/* ===== 合并下拉菜单 (fixed定位) ===== */
.sp-kb-dropdown-menu {
position: fixed;
@@ -293,6 +362,162 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
transition: all 0.2s;
}
.clip-action-btn:hover { background: rgba(7,193,96,0.08); color: var(--primary); }
/* === Markdown 渲染样式 === */
.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; }
.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; }
.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; }
.md-h { margin: 8px 0 4px; line-height: 1.4; }
h1.md-h { font-size: 18px; font-weight: 700; }
h2.md-h { font-size: 16px; font-weight: 700; }
h3.md-h { font-size: 14px; font-weight: 600; }
h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; }
.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; }
.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; }
.md-list li { margin: 2px 0; }
.md-list .md-list { margin: 2px 0 2px 0; }
.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; }
.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; }
.md-p a:hover, .md-list a:hover { text-decoration: underline; }
.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; }
.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; white-space: nowrap; }
.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; }
.md-table tr:hover td { background: rgba(7,193,96,0.02); }
.md-table-wrap { overflow-x: auto; margin: 6px 0; }
.md-img { max-width: 100%; max-height: 200px; border-radius: 6px; margin: 6px 0; display: block; object-fit: contain; cursor: pointer; }
.md-img:hover { opacity: 0.85; }
/* 图片灯箱 */
.sp-lightbox {
position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 9999;
display: flex; align-items: center; justify-content: center; cursor: zoom-out;
}
.sp-lightbox img {
max-width: 95%; max-height: 95%; object-fit: contain; border-radius: 8px;
box-shadow: 0 4px 32px rgba(0,0,0,0.4);
}
/* === Agent 流式显示(参考 AgentStreamDisplay.vue)=== */
.agent-stream { display: flex; flex-direction: column; gap: 0; width: 92%; align-self: flex-start; margin-bottom: 6px; }
/* 折叠的中间步骤 — 树根 */
.agent-tree-root {
display: flex; justify-content: space-between; align-items: center;
padding: 6px 14px; font-size: 12px; font-weight: 500; cursor: pointer;
border-radius: 8px; background: var(--white); border: 0.5px solid #e0e0e0;
box-shadow: 0 2px 4px rgba(7,192,95,0.08); color: var(--text);
transition: all 0.25s cubic-bezier(0.4,0,0.2,1); margin-bottom: 10px; user-select: none;
}
.agent-tree-root:hover { background: rgba(7,192,95,0.04); }
.agent-tree-root-title { display: flex; align-items: center; gap: 6px; }
.agent-tree-root-title svg { width: 16px; height: 16px; color: var(--primary); flex-shrink: 0; }
.agent-tree-root-toggle { font-size: 12px; color: var(--primary); transition: transform 0.2s; }
.agent-tree-root.expanded .agent-tree-root-toggle { transform: rotate(180deg); }
/* 树子节点 */
.agent-tree-children { display: none; padding-left: 12px; margin-top: -4px; margin-bottom: 10px; position: relative; }
.agent-tree-root.expanded + .agent-tree-children { display: block; }
.agent-tree-child { position: relative; padding-left: 20px; margin-bottom: 6px; }
.agent-tree-child::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; border-left: 1px dashed #e0e0e0; }
.agent-tree-child:last-child::before { bottom: auto; height: 15px; }
.agent-tree-branch { position: absolute; left: 0; top: 15px; width: 16px; border-top: 1px dashed #e0e0e0; }
/* 通用动作卡片(思考/工具调用) */
.agent-card {
background: var(--white); border-radius: 5px; border: 1px solid #e8e8e8;
overflow: hidden; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.02);
animation: agentFadeIn 0.25s ease-out; margin-bottom: 8px; position: relative; width: 100%;
}
.agent-card:hover { border-color: var(--primary); box-shadow: 0 1px 4px rgba(7,192,95,0.08); }
.agent-card.pending { border-color: rgba(7,192,95,0.15); overflow: hidden; }
.agent-card.pending::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(120deg, transparent 0%, rgba(7,192,95,0.06) 40%, rgba(7,192,95,0.08) 55%, transparent 85%);
transform: translateX(-100%); animation: agentShimmer 2.8s ease-in-out infinite; pointer-events: none;
}
.agent-card.error { border-left: 2px solid #e44; }
.agent-card-header {
display: flex; justify-content: space-between; align-items: center;
padding: 5px 10px; cursor: pointer; user-select: none; transition: background-color 0.15s;
}
.agent-card-header:hover { background: rgba(7,192,95,0.03); }
.agent-card-header.no-expand { cursor: default; }
.agent-card-header.no-expand:hover { background: transparent; }
.agent-card-title { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.agent-card-title svg { width: 14px; height: 14px; color: var(--primary); flex-shrink: 0; }
.agent-card-name { font-size: 12px; font-weight: 500; white-space: nowrap; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.agent-card-summary { color: var(--text3); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; margin-left: 4px; }
.agent-card-toggle { font-size: 12px; color: var(--text3); transition: transform 0.2s; padding: 0 2px; flex-shrink: 0; }
.agent-card.expanded .agent-card-toggle { transform: rotate(180deg); }
.agent-card-detail {
display: none; padding: 6px 12px; border-top: 1px solid #f0f0f0;
font-size: 12px; color: var(--text2); line-height: 1.6; max-height: 200px;
overflow-y: auto; background: #fafbfc; white-space: pre-wrap; word-break: break-word;
}
.agent-card.expanded .agent-card-detail { display: block; }
.agent-card-info {
padding: 5px 10px; border-top: 1px solid #f0f0f0;
font-size: 11px; font-weight: 500; color: var(--text); line-height: 1.5;
}
.agent-card-info strong { color: var(--primary); font-weight: 600; }
/* 计划任务变更 */
.agent-plan-change {
padding: 8px 12px; background: linear-gradient(135deg, rgba(7,192,95,0.05), rgba(7,192,95,0.02));
border-radius: 6px; border: 1px solid rgba(7,192,95,0.2);
font-size: 12px; color: var(--text); margin-bottom: 8px; animation: agentFadeIn 0.25s ease-out;
}
.agent-plan-change strong { color: var(--primary); font-weight: 600; margin-right: 3px; }
/* 回答区域 */
.agent-answer { animation: agentFadeIn 0.25s ease-out; }
.agent-answer-content {
font-size: 14px; color: var(--text); line-height: 1.6; background: var(--white);
border-radius: 16px; border-bottom-left-radius: 6px; padding: 12px 16px;
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08);
}
.agent-answer-toolbar {
display: flex; gap: 6px; margin-top: 6px; padding-left: 2px;
}
.agent-answer-toolbar button {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border: 1px solid #e8e8e8; border-radius: 50%;
background: var(--white); cursor: pointer; color: var(--text3); transition: all 0.2s;
}
.agent-answer-toolbar button:hover { border-color: var(--primary); color: var(--primary); background: rgba(7,192,95,0.04); }
.agent-answer-toolbar button svg { width: 14px; height: 14px; }
/* 知识引用面板 */
.agent-refs {
background: var(--white); border: 1px solid #e8e8e8; border-radius: 10px;
margin-top: 8px; overflow: hidden; font-size: 12px;
}
.agent-refs-header {
display: flex; align-items: center; gap: 6px; padding: 8px 12px;
cursor: pointer; color: var(--text2); font-weight: 500; user-select: none;
}
.agent-refs-header:hover { background: rgba(7,193,96,0.04); }
.agent-refs-header svg:first-child { color: var(--primary); flex-shrink: 0; }
.agent-refs-arrow { margin-left: auto; transition: transform 0.2s; color: var(--text3); }
.agent-refs.expanded .agent-refs-arrow { transform: rotate(180deg); }
.agent-refs-list {
display: none; border-top: 1px solid #f0f0f0; padding: 6px 12px 8px;
max-height: 200px; overflow-y: auto;
}
.agent-refs.expanded .agent-refs-list { display: block; }
.agent-ref-group { margin-bottom: 6px; }
.agent-ref-group-title { display: flex; align-items: center; gap: 4px; font-weight: 500; color: var(--text); padding: 2px 0; font-size: 12px; }
.agent-ref-group-title svg { color: var(--primary); flex-shrink: 0; }
.agent-ref-count { color: var(--text3); font-weight: 400; font-size: 11px; }
.agent-ref-chunk { color: var(--text3); padding: 2px 0 2px 18px; line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; font-size: 11px; }
.agent-ref-chunk:hover { color: var(--primary); background: rgba(7,193,96,0.04); border-radius: 4px; }
.agent-ref-idx { color: var(--primary); font-weight: 500; font-size: 10px; }
/* 动画 */
@keyframes agentFadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
@keyframes agentShimmer { 0% { transform: translateX(-90%); } 50% { transform: translateX(-5%); } 100% { transform: translateX(90%); } }
@keyframes agentSpin { to { transform: rotate(360deg); } }
.agent-spinner { width: 12px; height: 12px; border: 2px solid #e0e0e0; border-top-color: var(--primary); border-radius: 50%; animation: agentSpin 0.8s linear infinite; display: inline-block; }
</style>
</head>
<body>
@@ -301,7 +526,10 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
<div class="header-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h1>快速笔记 5.0</h1>
<div class="header-title-wrap">
<h1>快速笔记 5.0</h1>
<div class="header-session-title" id="sp-session-title"></div>
</div>
<button class="header-btn" id="btn-clear" title="清空对话">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
@@ -328,18 +556,31 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
</div>
<div class="input-area">
<div class="sp-chat-bar">
<button class="sp-chat-bar-kb" id="sp-btn-kb">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
<span id="sp-kb-name">全部</span>
<svg class="sp-chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="sp-chat-bar-divider"></div>
<input class="sp-chat-bar-input" id="sp-input" type="text" placeholder="向知识库提问…" autocomplete="off">
<button class="sp-chat-bar-send" id="sp-send">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
</button>
<div class="sp-image-previews" id="sp-image-previews" style="display:none;"></div>
<div class="sp-chat-bar-wrapper">
<div class="sp-chat-bar-toolbar">
<button class="sp-chat-bar-kb" id="sp-btn-kb">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
<span id="sp-kb-name">全部</span>
<svg class="sp-chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<button class="sp-chat-bar-mode" id="sp-btn-mode">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
<span id="sp-mode-name">快速回答</span>
<svg class="sp-chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
</div>
<div class="sp-chat-bar">
<button class="sp-chat-bar-img" id="sp-image-btn" style="display:none;" title="上传图片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</button>
<textarea class="sp-chat-bar-input" id="sp-input" placeholder="向知识库提问…" autocomplete="off" rows="1"></textarea>
<button class="sp-chat-bar-send" id="sp-send">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
</button>
</div>
</div>
<input type="file" id="sp-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
<!-- 合并下拉菜单 -->
<div class="sp-kb-dropdown-menu" id="sp-kb-menu">
<div class="sp-dropdown-item selected" data-kb="all">
@@ -367,8 +608,9 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
技术规范
</div>
<div class="sp-dropdown-divider"></div>
<div class="sp-dropdown-section">模式</div>
</div>
<!-- 模式下拉菜单(独立) -->
<div class="sp-mode-dropdown-menu" id="sp-mode-menu">
<div class="sp-mode-item selected" data-mode="fast">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
@@ -384,11 +626,6 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
精确
</div>
<div class="sp-dropdown-divider"></div>
<div class="sp-attach-item" id="sp-btn-attach">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
上传图片
</div>
</div>
</div>
File diff suppressed because it is too large Load Diff
@@ -106,6 +106,123 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
.chat-tool-btn svg { width: 12px; height: 12px; }
.chat-tool-btn.mode-active { color: var(--primary); font-weight: 500; }
.chat-tool-divider { width: 1px; height: 12px; background: #eee; margin: 0 2px; }
/* 图片上传按钮 */
.wk-image-upload-btn {
display: flex; align-items: center; justify-content: center;
border: none; background: none; color: #999; cursor: pointer;
width: 26px; height: 26px; padding: 0; border-radius: 6px; transition: all 0.15s;
}
.wk-image-upload-btn:hover { color: var(--primary); background: rgba(7,193,96,0.06); }
.wk-image-upload-btn svg { width: 14px; height: 14px; }
/* 图片预览条 */
.wk-image-previews {
display: flex; gap: 6px; padding: 6px 12px; overflow-x: auto; flex-shrink: 0;
}
.wk-img-item { position: relative; flex-shrink: 0; }
.wk-img-thumb { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; }
.wk-img-remove {
position: absolute; top: -4px; right: -4px;
width: 16px; height: 16px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff;
font-size: 12px; line-height: 16px; text-align: center; cursor: pointer; border: none; padding: 0;
}
.wk-img-remove:hover { background: #e44; }
/* === Markdown 渲染样式 === */
.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; }
.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; }
.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; }
.md-h { margin: 8px 0 4px; line-height: 1.4; }
h1.md-h { font-size: 18px; font-weight: 700; }
h2.md-h { font-size: 16px; font-weight: 700; }
h3.md-h { font-size: 14px; font-weight: 600; }
h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; }
.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; }
.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; }
.md-list li { margin: 2px 0; }
.md-list .md-list { margin: 2px 0 2px 0; }
.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; }
.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; }
.md-p a:hover, .md-list a:hover { text-decoration: underline; }
.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; width: 100%; }
.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; }
.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; }
.md-table tr:hover td { background: rgba(7,193,96,0.02); }
.md-img { max-width: 100%; max-height: 200px; border-radius: 6px; margin: 6px 0; display: block; object-fit: contain; cursor: pointer; }
/* === Agent 流式显示 === */
.agent-stream { display: flex; flex-direction: column; gap: 0; max-width: 90%; margin-bottom: 6px; }
.agent-tree-root {
display: flex; justify-content: space-between; align-items: center;
padding: 6px 12px; font-size: 12px; font-weight: 500; cursor: pointer;
border-radius: 8px; background: #fff; border: 0.5px solid #e0e0e0;
box-shadow: 0 2px 4px rgba(7,192,95,0.08); color: var(--text);
transition: all 0.25s; margin-bottom: 8px; user-select: none;
}
.agent-tree-root:hover { background: rgba(7,192,95,0.04); }
.agent-tree-root-title { display: flex; align-items: center; gap: 6px; }
.agent-tree-root-title svg { width: 16px; height: 16px; color: var(--primary); flex-shrink: 0; }
.agent-tree-root-toggle { font-size: 12px; color: var(--primary); transition: transform 0.2s; }
.agent-tree-root.expanded .agent-tree-root-toggle { transform: rotate(180deg); }
.agent-tree-children { display: none; padding-left: 12px; margin-top: -4px; margin-bottom: 8px; }
.agent-tree-root.expanded + .agent-tree-children { display: block; }
.agent-tree-child { position: relative; padding-left: 20px; margin-bottom: 6px; }
.agent-tree-child::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; border-left: 1px dashed #e0e0e0; }
.agent-tree-child:last-child::before { bottom: auto; height: 15px; }
.agent-tree-branch { position: absolute; left: 0; top: 15px; width: 16px; border-top: 1px dashed #e0e0e0; }
.agent-card {
background: #fff; border-radius: 5px; border: 1px solid #e8e8e8;
overflow: hidden; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.02);
animation: agentFadeIn 0.25s ease-out; margin-bottom: 8px; position: relative;
}
.agent-card:hover { border-color: var(--primary); box-shadow: 0 1px 4px rgba(7,192,95,0.08); }
.agent-card.pending { border-color: rgba(7,192,95,0.15); }
.agent-card.pending::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(120deg, transparent 0%, rgba(7,192,95,0.06) 40%, rgba(7,192,95,0.08) 55%, transparent 85%);
transform: translateX(-100%); animation: agentShimmer 2.8s ease-in-out infinite; pointer-events: none;
}
.agent-card.error { border-left: 2px solid #e44; }
.agent-card-header { display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; cursor: pointer; user-select: none; transition: background-color 0.15s; }
.agent-card-header:hover { background: rgba(7,192,95,0.03); }
.agent-card-header.no-expand { cursor: default; }
.agent-card-header.no-expand:hover { background: transparent; }
.agent-card-title { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.agent-card-title svg { width: 14px; height: 14px; color: var(--primary); flex-shrink: 0; }
.agent-card-name { font-size: 12px; font-weight: 500; white-space: nowrap; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.agent-card-summary { color: #999; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; margin-left: 4px; }
.agent-card-toggle { font-size: 12px; color: #999; transition: transform 0.2s; padding: 0 2px; flex-shrink: 0; }
.agent-card.expanded .agent-card-toggle { transform: rotate(180deg); }
.agent-card-detail { display: none; padding: 6px 12px; border-top: 1px solid #f0f0f0; font-size: 12px; color: #555; line-height: 1.6; max-height: 200px; overflow-y: auto; background: #fafbfc; white-space: pre-wrap; word-break: break-word; }
.agent-card.expanded .agent-card-detail { display: block; }
.agent-card-info { padding: 5px 10px; border-top: 1px solid #f0f0f0; font-size: 11px; font-weight: 500; color: var(--text); }
.agent-card-info strong { color: var(--primary); font-weight: 600; }
.agent-answer { animation: agentFadeIn 0.25s ease-out; }
.agent-answer-content { font-size: 13px; color: #333; line-height: 1.6; background: #f7f8fa; padding: 10px 14px; border-radius: 12px; border-bottom-left-radius: 4px; }
.agent-answer-toolbar { display: flex; gap: 6px; margin-top: 6px; }
.agent-answer-toolbar button { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: 1px solid #e8e8e8; border-radius: 50%; background: #fff; cursor: pointer; color: #999; transition: all 0.2s; }
.agent-answer-toolbar button:hover { border-color: var(--primary); color: var(--primary); }
.agent-answer-toolbar button svg { width: 13px; height: 13px; }
.agent-refs { background: #fff; border: 1px solid #e8e8e8; border-radius: 10px; margin-top: 8px; overflow: hidden; font-size: 12px; }
.agent-refs-header { display: flex; align-items: center; gap: 6px; padding: 8px 12px; cursor: pointer; color: #555; font-weight: 500; user-select: none; }
.agent-refs-header:hover { background: rgba(7,193,96,0.04); }
.agent-refs-header svg:first-child { color: var(--primary); flex-shrink: 0; }
.agent-refs-arrow { margin-left: auto; transition: transform 0.2s; color: #999; }
.agent-refs.expanded .agent-refs-arrow { transform: rotate(180deg); }
.agent-refs-list { display: none; border-top: 1px solid #f0f0f0; padding: 6px 12px 8px; max-height: 200px; overflow-y: auto; }
.agent-refs.expanded .agent-refs-list { display: block; }
.agent-ref-group { margin-bottom: 6px; }
.agent-ref-group-title { display: flex; align-items: center; gap: 4px; font-weight: 500; color: var(--text); padding: 2px 0; font-size: 12px; }
.agent-ref-group-title svg { color: var(--primary); flex-shrink: 0; }
.agent-ref-count { color: #999; font-weight: 400; font-size: 11px; }
.agent-ref-chunk { color: #999; padding: 2px 0 2px 18px; line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; font-size: 11px; }
.agent-ref-chunk:hover { color: var(--primary); background: rgba(7,193,96,0.04); border-radius: 4px; }
.agent-ref-idx { color: var(--primary); font-weight: 500; font-size: 10px; }
@keyframes agentFadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
@keyframes agentShimmer { 0% { transform: translateX(-90%); } 50% { transform: translateX(-5%); } 100% { transform: translateX(90%); } }
@keyframes agentSpin { to { transform: rotate(360deg); } }
.agent-spinner { width: 12px; height: 12px; border: 2px solid #e0e0e0; border-top-color: var(--primary); border-radius: 50%; animation: agentSpin 0.8s linear infinite; display: inline-block; }
</style>
</head>
<body>
@@ -146,10 +263,10 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
</div>
</div>
<div class="sidebar-user">
<div class="sidebar-avatar">S</div>
<div class="sidebar-avatar" id="wk-user-letter">U</div>
<div class="sidebar-user-info">
<div class="sidebar-user-name">saras</div>
<div class="sidebar-user-email">saraszhang@tencent....</div>
<div class="sidebar-user-name" id="wk-user-name">加载中...</div>
<div class="sidebar-user-email" id="wk-user-email"></div>
</div>
<svg class="sidebar-user-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
@@ -236,9 +353,13 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
<p>选择知识库后,输入问题开始智能问答</p>
</div>
<div class="chat-panel-input">
<div class="wk-image-previews" id="wk-image-previews" style="display:none;"></div>
<div class="chat-input-card">
<textarea placeholder="输入问题..." rows="1"></textarea>
<div class="chat-input-toolbar">
<button class="wk-image-upload-btn" id="wk-image-btn" style="display:none;" title="上传图片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</button>
<button class="chat-tool-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
普通
@@ -258,6 +379,7 @@ body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
</div>
</div>
</div>
<input type="file" id="wk-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
</div>
<script src="weknora.js"></script>
File diff suppressed because it is too large Load Diff