Files
yu-zhi-ran/platform/frontend/ai-assistant.js
T

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(); };
})();