362 lines
12 KiB
JavaScript
362 lines
12 KiB
JavaScript
(function () {
|
|
if (document.getElementById('ai-assistant-styles')) return;
|
|
|
|
var style = document.createElement('style');
|
|
style.id = 'ai-assistant-styles';
|
|
style.textContent = '\
|
|
.ai-assistant-btn {\
|
|
position: fixed; bottom: 24px; right: 24px;\
|
|
z-index: 99999;\
|
|
width: 52px; height: 52px;\
|
|
border-radius: 50%;\
|
|
background: linear-gradient(135deg, #2563eb, #1d4ed8);\
|
|
color: #fff;\
|
|
border: none;\
|
|
cursor: pointer;\
|
|
box-shadow: 0 4px 16px rgba(37,99,235,0.35);\
|
|
display: flex; align-items: center; justify-content: center;\
|
|
font-size: 22px;\
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\
|
|
}\
|
|
.ai-assistant-btn:hover { transform: scale(1.1); box-shadow: 0 6px 24px rgba(37,99,235,0.45); }\
|
|
.ai-assistant-btn.open { transform: rotate(45deg); background: #f56c6c; box-shadow: 0 4px 16px rgba(245,108,108,0.35); }\
|
|
.ai-assistant-panel {\
|
|
position: fixed; bottom: 88px; right: 24px;\
|
|
z-index: 99998;\
|
|
width: 380px; height: 560px;\
|
|
background: #fff;\
|
|
border-radius: 16px;\
|
|
box-shadow: 0 8px 40px rgba(0,0,0,0.15);\
|
|
display: none;\
|
|
flex-direction: column;\
|
|
overflow: hidden;\
|
|
animation: aiSlideUp 0.3s ease-out;\
|
|
border: 1px solid #e5e7eb;\
|
|
}\
|
|
.ai-assistant-panel.open { display: flex; }\
|
|
@keyframes aiSlideUp {\
|
|
from { opacity: 0; transform: translateY(20px) scale(0.95); }\
|
|
to { opacity: 1; transform: translateY(0) scale(1); }\
|
|
}\
|
|
.ai-assistant-header {\
|
|
padding: 14px 16px;\
|
|
background: linear-gradient(135deg, #2563eb, #1d4ed8);\
|
|
color: #fff;\
|
|
font-weight: 600;\
|
|
font-size: 15px;\
|
|
display: flex; align-items: center; gap: 8px;\
|
|
flex-shrink: 0;\
|
|
}\
|
|
.ai-assistant-header .ai-badge {\
|
|
background: rgba(255,255,255,0.2);\
|
|
padding: 2px 8px; border-radius: 10px;\
|
|
font-size: 10px; font-weight: 400;\
|
|
}\
|
|
.ai-assistant-messages {\
|
|
flex: 1; overflow-y: auto;\
|
|
padding: 12px 16px;\
|
|
background: #f8f9fb;\
|
|
display: flex; flex-direction: column; gap: 10px;\
|
|
}\
|
|
.ai-assistant-msg {\
|
|
max-width: 85%;\
|
|
padding: 10px 14px;\
|
|
border-radius: 12px;\
|
|
font-size: 13px;\
|
|
line-height: 1.6;\
|
|
word-break: break-word;\
|
|
white-space: pre-wrap;\
|
|
}\
|
|
.ai-assistant-msg.ai {\
|
|
background: #fff;\
|
|
border: 1px solid #e5e7eb;\
|
|
color: #303133;\
|
|
align-self: flex-start;\
|
|
border-bottom-left-radius: 4px;\
|
|
}\
|
|
.ai-assistant-msg.user {\
|
|
background: #2563eb;\
|
|
color: #fff;\
|
|
align-self: flex-end;\
|
|
border-bottom-right-radius: 4px;\
|
|
}\
|
|
.ai-assistant-msg .msg-time {\
|
|
font-size: 10px; opacity: 0.6;\
|
|
margin-top: 4px; display: block;\
|
|
}\
|
|
.ai-assistant-typing {\
|
|
align-self: flex-start;\
|
|
display: flex; gap: 4px;\
|
|
padding: 12px 16px;\
|
|
background: #fff;\
|
|
border: 1px solid #e5e7eb;\
|
|
border-radius: 12px;\
|
|
border-bottom-left-radius: 4px;\
|
|
}\
|
|
.ai-assistant-typing span {\
|
|
width: 6px; height: 6px;\
|
|
background: #909399;\
|
|
border-radius: 50%;\
|
|
animation: aiBounce 1.4s ease-in-out infinite;\
|
|
}\
|
|
.ai-assistant-typing span:nth-child(2) { animation-delay: 0.2s; }\
|
|
.ai-assistant-typing span:nth-child(3) { animation-delay: 0.4s; }\
|
|
@keyframes aiBounce {\
|
|
0%, 60%, 100% { transform: translateY(0); }\
|
|
30% { transform: translateY(-6px); }\
|
|
}\
|
|
.ai-assistant-input {\
|
|
display: flex; align-items: center;\
|
|
padding: 10px 12px;\
|
|
border-top: 1px solid #e5e7eb;\
|
|
background: #fff;\
|
|
flex-shrink: 0;\
|
|
gap: 8px;\
|
|
}\
|
|
.ai-assistant-input textarea {\
|
|
flex: 1;\
|
|
border: 1px solid #d1d5db;\
|
|
border-radius: 8px;\
|
|
padding: 8px 12px;\
|
|
font-size: 13px;\
|
|
outline: none;\
|
|
resize: none;\
|
|
height: 36px;\
|
|
line-height: 20px;\
|
|
font-family: inherit;\
|
|
transition: border-color 0.2s;\
|
|
}\
|
|
.ai-assistant-input textarea:focus { border-color: #2563eb; }\
|
|
.ai-assistant-send {\
|
|
width: 36px; height: 36px;\
|
|
border-radius: 8px;\
|
|
background: #2563eb;\
|
|
color: #fff;\
|
|
border: none;\
|
|
cursor: pointer;\
|
|
font-size: 16px;\
|
|
display: flex; align-items: center; justify-content: center;\
|
|
transition: all 0.2s;\
|
|
flex-shrink: 0;\
|
|
}\
|
|
.ai-assistant-send:hover { background: #1d4ed8; }\
|
|
.ai-assistant-send:disabled { background: #c0c4cc; cursor: not-allowed; }\
|
|
.ai-assistant-greeting {\
|
|
text-align: center;\
|
|
padding: 20px 16px;\
|
|
color: #909399;\
|
|
font-size: 13px;\
|
|
}\
|
|
.ai-assistant-greeting .greeting-icon {\
|
|
font-size: 36px; margin-bottom: 8px;\
|
|
}\
|
|
.ai-assistant-greeting h4 {\
|
|
color: #303133; font-size: 15px;\
|
|
margin: 0 0 6px;\
|
|
}\
|
|
@media (max-width: 480px) {\
|
|
.ai-assistant-panel {\
|
|
right: 12px; left: 12px;\
|
|
width: auto;\
|
|
height: 480px;\
|
|
bottom: 80px;\
|
|
}\
|
|
.ai-assistant-btn { right: 16px; bottom: 16px; }\
|
|
}\
|
|
';
|
|
document.head.appendChild(style);
|
|
|
|
var token = localStorage.getItem('authToken');
|
|
if (!token) return;
|
|
|
|
var pageMap = {
|
|
'/': 'dashboard', '/index.html': 'dashboard',
|
|
'topics.html': 'topics', 'metrics.html': 'metrics',
|
|
'calendar.html': 'calendar', 'assets.html': 'assets',
|
|
'tasks.html': 'tasks', 'platforms.html': 'platforms',
|
|
'admin.html': 'admin'
|
|
};
|
|
var path = window.location.pathname;
|
|
var currentPage = pageMap[path] || pageMap['/'];
|
|
|
|
var greettings = {
|
|
'dashboard': '👋 欢迎回来!我可以帮你了解仪表盘上的统计数据、模块状态的含义,或指导你如何快速定位待处理选题。',
|
|
'topics': '📝 这里是选题管理。我可以帮你了解如何创建选题、设置评分、编辑内容,或如何使用预览功能查看各平台文章效果。',
|
|
'metrics': '📊 数据分析页展示内容表现。我可以帮你解读图表数据、分析各平台效果差异,或建议如何提升内容互动率。',
|
|
'calendar': '📅 内容日历帮你规划发布节奏。我可以教你如何创建计划、调整日期,或优化内容排期策略。',
|
|
'assets': '🖼️ 素材库管理图片和文件。我可以指导你如何上传素材、添加标签分类,或关联到相关选题。',
|
|
'tasks': '⚡ 任务页面跟踪所有创作任务。我可以解释各任务的含义、查看执行进度,或排查任务失败原因。',
|
|
'platforms': '🔗 平台配置管理各发布渠道。我可以帮你了解如何配置平台参数、设置发布格式。',
|
|
'admin': '⚙️ 系统管理页面。我可以帮你管理用户、配置 LLM、查看运行日志,或设置采集类别和信息源。',
|
|
};
|
|
|
|
var HISTORY_KEY = 'ai_assistant_history_' + currentPage;
|
|
var saved = [];
|
|
try { saved = JSON.parse(localStorage.getItem(HISTORY_KEY)) || []; } catch(e) {}
|
|
|
|
var container = document.createElement('div');
|
|
container.innerHTML = '\
|
|
<button class="ai-assistant-btn" id="aiAssistantBtn" title="AI 助手">\
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\
|
|
<path d="M12 2a4 4 0 0 1 4 4v2a4 4 0 0 1-8 0V6a4 4 0 0 1 4-4z"/>\
|
|
<path d="M16 14H8a4 4 0 0 0-4 4v2h16v-2a4 4 0 0 0-4-4z"/>\
|
|
<circle cx="9" cy="10" r="1" fill="currentColor"/>\
|
|
<circle cx="15" cy="10" r="1" fill="currentColor"/>\
|
|
<path d="M12 14v2"/>\
|
|
</svg>\
|
|
</button>\
|
|
<div class="ai-assistant-panel" id="aiAssistantPanel">\
|
|
<div class="ai-assistant-header">\
|
|
<span>🤖 AI 助手</span>\
|
|
<span class="ai-badge">' + currentPage + '</span>\
|
|
</div>\
|
|
<div class="ai-assistant-messages" id="aiMessages">\
|
|
</div>\
|
|
<div class="ai-assistant-input">\
|
|
<textarea id="aiInput" rows="1" placeholder="输入你的问题..." maxlength="500"></textarea>\
|
|
<button class="ai-assistant-send" id="aiSendBtn">\
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\
|
|
<line x1="22" y1="2" x2="11" y2="13"/>\
|
|
<polygon points="22 2 15 22 11 13 2 9 22 2"/>\
|
|
</svg>\
|
|
</button>\
|
|
</div>\
|
|
</div>\
|
|
';
|
|
document.body.appendChild(container);
|
|
|
|
var btn = document.getElementById('aiAssistantBtn');
|
|
var panel = document.getElementById('aiAssistantPanel');
|
|
var messagesEl = document.getElementById('aiMessages');
|
|
var input = document.getElementById('aiInput');
|
|
var sendBtn = document.getElementById('aiSendBtn');
|
|
|
|
function addMessage(role, content) {
|
|
var div = document.createElement('div');
|
|
div.className = 'ai-assistant-msg ' + role;
|
|
div.textContent = content;
|
|
messagesEl.appendChild(div);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
}
|
|
|
|
function showGreeting() {
|
|
messagesEl.innerHTML = '\
|
|
<div class="ai-assistant-greeting">\
|
|
<div class="greeting-icon">🤖</div>\
|
|
<h4>宇之然 AI 助手</h4>\
|
|
<p>' + (greettings[currentPage] || '👋 你好!有什么可以帮助你的?') + '</p>\
|
|
</div>';
|
|
}
|
|
|
|
function showTyping() {
|
|
var div = document.createElement('div');
|
|
div.className = 'ai-assistant-typing';
|
|
div.id = 'aiTyping';
|
|
div.innerHTML = '<span></span><span></span><span></span>';
|
|
messagesEl.appendChild(div);
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
}
|
|
|
|
function removeTyping() {
|
|
var t = document.getElementById('aiTyping');
|
|
if (t) t.remove();
|
|
}
|
|
|
|
function saveHistory(history) {
|
|
try {
|
|
localStorage.setItem(HISTORY_KEY, JSON.stringify(history.slice(-50)));
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function sendMessage(text) {
|
|
if (!text.trim()) return;
|
|
input.value = '';
|
|
input.style.height = '36px';
|
|
addMessage('user', text);
|
|
if (messagesEl.querySelector('.ai-assistant-greeting')) {
|
|
messagesEl.querySelector('.ai-assistant-greeting').remove();
|
|
}
|
|
sendBtn.disabled = true;
|
|
|
|
var history = [];
|
|
var msgs = messagesEl.querySelectorAll('.ai-assistant-msg');
|
|
msgs.forEach(function(m) {
|
|
history.push({ role: m.classList.contains('user') ? 'user' : 'assistant', content: m.textContent });
|
|
});
|
|
|
|
showTyping();
|
|
try {
|
|
var resp = await fetch('/api/assistant/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|
body: JSON.stringify({ message: text, page: currentPage, history: history })
|
|
});
|
|
removeTyping();
|
|
if (resp.ok) {
|
|
var data = await resp.json();
|
|
addMessage('assistant', data.reply || '已收到');
|
|
saveHistory(history.concat([
|
|
{ role: 'user', content: text },
|
|
{ role: 'assistant', content: data.reply || '已收到' }
|
|
]));
|
|
} else {
|
|
addMessage('assistant', '抱歉,请求失败,请稍后重试。');
|
|
}
|
|
} catch(e) {
|
|
removeTyping();
|
|
addMessage('assistant', '网络异常,请检查连接后重试。');
|
|
}
|
|
sendBtn.disabled = false;
|
|
input.focus();
|
|
}
|
|
|
|
// Open/close
|
|
var isOpen = false;
|
|
btn.addEventListener('click', function() {
|
|
isOpen = !isOpen;
|
|
btn.classList.toggle('open', isOpen);
|
|
panel.classList.toggle('open', isOpen);
|
|
if (isOpen) {
|
|
var msgs = messagesEl.querySelectorAll('.ai-assistant-msg');
|
|
if (msgs.length === 0) {
|
|
// Load saved or show greeting
|
|
if (saved.length > 0) {
|
|
saved.forEach(function(m) { addMessage(m.role, m.content); });
|
|
} else {
|
|
showGreeting();
|
|
}
|
|
}
|
|
input.focus();
|
|
}
|
|
});
|
|
|
|
// Send on click
|
|
sendBtn.addEventListener('click', function() { sendMessage(input.value); });
|
|
|
|
// Send on Enter (Shift+Enter for newline)
|
|
input.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage(input.value);
|
|
}
|
|
});
|
|
|
|
// Auto-resize textarea
|
|
input.addEventListener('input', function() {
|
|
this.style.height = '36px';
|
|
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
});
|
|
|
|
// Close on Escape
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && isOpen) {
|
|
isOpen = false;
|
|
btn.classList.remove('open');
|
|
panel.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
window.aiAssistantOpen = function() { btn.click(); };
|
|
window.aiAssistantClose = function() { if (isOpen) btn.click(); };
|
|
})();
|