Files
ai-chat-system/templates/index.html

475 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 对话系统 v2.0</title>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.2.0/github-markdown-light.min.css" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; height: 100vh; display: flex; }
.sidebar { width: 260px; background: #202123; color: #fff; display: flex; flex-direction: column; }
.sidebar-header { padding: 16px; border-bottom: 1px solid #4d4d4f; }
.new-chat-btn { width: 100%; padding: 12px 16px; background: transparent; border: 1px solid #4d4d4f; border-radius: 6px; color: #fff; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: 14px; transition: background 0.2s; }
.new-chat-btn:hover { background: #2a2b32; }
.conversation-list { flex: 1; overflow-y: auto; padding: 8px; }
.conversation-item { padding: 12px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; color: #ececf1; }
.conversation-item:hover { background: #2a2b32; }
.conversation-item.active { background: #343541; }
.conversation-item .title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
.conversation-item .delete-btn { opacity: 0; background: none; border: none; color: #999; cursor: pointer; padding: 4px; }
.conversation-item:hover .delete-btn { opacity: 1; }
.main-content { flex: 1; display: flex; flex-direction: column; background: #fff; }
.chat-header { padding: 16px 24px; border-bottom: 1px solid #e5e5e5; display: flex; align-items: center; justify-content: space-between; }
.chat-header h1 { font-size: 18px; font-weight: 600; }
.header-controls { display: flex; align-items: center; gap: 16px; }
.agent-selector { display: flex; align-items: center; gap: 8px; }
.agent-selector select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; cursor: pointer; min-width: 150px; }
.ws-status { font-size: 12px; color: #666; background: #f0f0f0; padding: 4px 8px; border-radius: 4px; }
.ws-status.connected { color: #10a37f; }
.messages-container { flex: 1; overflow-y: auto; padding: 24px; }
.message { max-width: 800px; margin: 0 auto 24px; display: flex; gap: 16px; }
.message-avatar { width: 36px; height: 36px; border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 18px; }
.message.user .message-avatar { background: #5436da; color: #fff; }
.message.assistant .message-avatar { background: #19c37d; color: #fff; }
.message-body { flex: 1; }
.message-content { line-height: 1.6; position: relative; }
/* Markdown样式 */
.markdown-body { font-size: 15px; }
.markdown-body pre { background: #f6f8fa; border-radius: 6px; padding: 16px; overflow-x: auto; }
.markdown-body code { background: #f6f8fa; padding: 2px 6px; border-radius: 4px; font-size: 85%; }
.markdown-body pre code { background: transparent; padding: 0; }
.markdown-body blockquote { border-left: 4px solid #dfe2e5; padding-left: 16px; color: #6a737d; }
/* 用户消息样式 */
.user-message-text { background: #f0f0f0; padding: 12px 16px; border-radius: 12px; font-size: 15px; }
/* 复制按钮 */
.copy-btn { position: absolute; top: 8px; right: 8px; padding: 4px 8px; background: rgba(0,0,0,0.05); border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-size: 12px; color: #666; opacity: 0; transition: opacity 0.2s; }
.message-content:hover .copy-btn { opacity: 1; }
.copy-btn:hover { background: rgba(0,0,0,0.1); }
.copy-btn.copied { color: #10a37f; border-color: #10a37f; }
/* 思考内容样式 */
.thinking-block { background: #f8f9fa; border-left: 3px solid #667eea; padding: 12px 16px; margin: 8px 0 16px 0; font-size: 14px; color: #666; border-radius: 4px; position: relative; }
.thinking-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
.thinking-header span { font-weight: 500; color: #667eea; }
.thinking-toggle { font-size: 12px; color: #667eea; }
.thinking-content { margin-top: 12px; display: none; }
.thinking-content.expanded { display: block; }
/* Agent信息 */
.agent-info { font-size: 12px; color: #999; margin-top: 8px; }
/* 输入区域 */
.input-container { padding: 16px 24px; border-top: 1px solid #e5e5e5; }
.input-area { max-width: 800px; margin: 0 auto; }
.input-row { display: flex; gap: 12px; align-items: center; }
.input-row textarea { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 12px; font-size: 16px; resize: none; outline: none; max-height: 200px; min-height: 48px; line-height: 1.5; }
.input-row textarea:focus { border-color: #10a37f; }
.send-btn { width: 48px; height: 48px; border-radius: 12px; background: #10a37f; border: none; color: #fff; cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center; transition: background 0.2s; }
.send-btn:hover { background: #0d8c6d; }
.send-btn:disabled { background: #ccc; cursor: not-allowed; }
/* 快捷语句 - 横向扁平 */
.quick-phrases-bar { display: flex; align-items: center; gap: 8px; margin-top: 12px; position: relative; }
.add-phrase-btn { padding: 6px 10px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; font-size: 12px; color: #666; white-space: nowrap; flex-shrink: 0; }
.add-phrase-btn:hover { background: #e8e8e8; }
.phrase-list-wrapper { flex: 1; overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }
.phrase-list-wrapper::-webkit-scrollbar { height: 4px; }
.phrase-list-wrapper::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
.phrase-list { display: flex; gap: 6px; padding: 4px 0; }
.phrase-tag { padding: 6px 12px; background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 16px; cursor: pointer; font-size: 13px; color: #333; white-space: nowrap; transition: all 0.2s; position: relative; }
.phrase-tag:hover { background: #e8f5e9; border-color: #10a37f; }
.phrase-tag .tag-delete { display: none; margin-left: 6px; color: #999; }
.phrase-tag:hover .tag-delete { display: inline; }
/* 弹窗 */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.modal-overlay.show { display: flex; }
.modal-box { background: #fff; padding: 24px; border-radius: 12px; max-width: 400px; width: 90%; }
.modal-box h3 { margin-bottom: 16px; }
.modal-box input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 16px; }
.modal-buttons { display: flex; gap: 12px; justify-content: flex-end; }
.modal-buttons button { padding: 8px 16px; border-radius: 8px; cursor: pointer; }
.welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #666; }
.welcome h2 { font-size: 28px; margin-bottom: 16px; color: #333; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; }
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" onclick="createNewConversation()"><i class="ri-add-line"></i> 新对话</button>
</div>
<div class="conversation-list" id="conversationList"></div>
</div>
<div class="main-content">
<div class="chat-header">
<h1>AI 对话 v2.0</h1>
<div class="header-controls">
<div class="agent-selector">
<select id="agentSelect" onchange="switchAgent()"><option value="">加载中...</option></select>
</div>
<div class="ws-status" id="wsStatus">连接中...</div>
</div>
</div>
<div class="messages-container" id="messagesContainer">
<div class="welcome"><h2>👋 开始对话</h2><p>选择Agent开始聊天</p></div>
</div>
<div class="input-container">
<div class="input-area">
<div class="input-row">
<textarea id="messageInput" placeholder="输入消息..." rows="1"></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()"><i class="ri-send-plane-fill"></i></button>
</div>
<div class="quick-phrases-bar">
<button class="add-phrase-btn" onclick="showAddPhraseModal()"><i class="ri-add-line"></i> 添加</button>
<div class="phrase-list-wrapper" id="phraseListWrapper" onwheel="scrollPhrases(event)">
<div class="phrase-list" id="quickPhrasesList"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加快捷语句弹窗 -->
<div class="modal-overlay" id="phraseModal">
<div class="modal-box">
<h3><i class="ri-add-line"></i> 添加快捷语句</h3>
<input type="text" id="newPhraseInput" placeholder="输入内容..." maxlength="100">
<div class="modal-buttons">
<button style="background:#f5f5f5;border:1px solid #ddd;" onclick="hidePhraseModal()">取消</button>
<button style="background:#10a37f;color:#fff;border:none;" onclick="addPhrase()">添加</button>
</div>
</div>
</div>
<!-- Markdown渲染库 -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
marked.setOptions({ breaks: true, gfm: true });
let ws = null;
let userId = 'main_user';
let currentConversationId = null;
let currentAgentId = null;
let agents = [];
let quickPhrases = [];
document.addEventListener('DOMContentLoaded', () => {
loadAgents();
loadQuickPhrases();
connectWebSocket();
loadConversations();
setupTextarea();
});
// 加载Agent
async function loadAgents() {
try {
const res = await fetch('/api/v2/agents');
const data = await res.json();
agents = data.agents || [];
const defaultAgent = agents.find(a => a.is_default) || agents[0];
if (defaultAgent) currentAgentId = defaultAgent.id;
renderAgentSelect();
} catch (e) { console.error('加载Agent失败:', e); }
}
function renderAgentSelect() {
const select = document.getElementById('agentSelect');
select.innerHTML = agents.filter(a => a.is_active).map(a =>
`<option value="${a.id}" ${a.id === currentAgentId ? 'selected' : ''}>${a.display_name || a.name}</option>`
).join('');
}
function updateAgentSelect(agentId) {
currentAgentId = agentId;
document.getElementById('agentSelect').value = agentId;
}
async function switchAgent() {
const newAgentId = parseInt(document.getElementById('agentSelect').value);
if (newAgentId && newAgentId !== currentAgentId) {
currentAgentId = newAgentId;
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ action: 'switch_agent', agent_id: currentAgentId }));
await createNewConversation();
showAgentSwitchNotice();
}
}
function showAgentSwitchNotice() {
const agent = agents.find(a => a.id === currentAgentId);
document.getElementById('messagesContainer').innerHTML = `
<div class="welcome"><h2>🔄 已切换 Agent</h2><p style="color:#667eea;font-size:18px;"><strong>${agent?.display_name || '助手'}</strong></p></div>
`;
}
// WebSocket
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws/${userId}`);
ws.onopen = () => { document.getElementById('wsStatus').textContent = '已连接'; document.getElementById('wsStatus').classList.add('connected'); };
ws.onmessage = (e) => handleWebSocketMessage(JSON.parse(e.data));
ws.onclose = () => { document.getElementById('wsStatus').textContent = '断开'; document.getElementById('wsStatus').classList.remove('connected'); setTimeout(connectWebSocket, 3000); };
ws.onerror = () => document.getElementById('wsStatus').textContent = '错误';
}
function handleWebSocketMessage(data) {
switch (data.type) {
case 'history': displayHistory(data.messages); if (data.agent_id) updateAgentSelect(data.agent_id); break;
case 'conversation_created': currentConversationId = data.conversation_id; loadConversations(); break;
case 'new_conversation': currentConversationId = data.conversation_id; loadConversations(); clearMessages(); break;
case 'agent_switched': currentAgentId = data.agent_id; break;
case 'stream_end': document.getElementById('sendBtn').disabled = false; break;
case 'user_message': appendMessage('user', data.message.content); break;
case 'assistant_message': appendMessage('assistant', data.message.content, data.message.thinking_content, data.message.agent_name); document.getElementById('sendBtn').disabled = false; break;
case 'error': showError(data.message); document.getElementById('sendBtn').disabled = false; break;
}
}
// 消息渲染
function appendMessage(role, content, thinking = null, agentName = null) {
const container = document.getElementById('messagesContainer');
container.querySelector('.welcome')?.remove();
const div = document.createElement('div');
div.className = `message ${role}`;
const avatar = role === 'user' ? '👤' : '🤖';
let html = `<div class="message-avatar">${avatar}</div><div class="message-body">`;
// 思考内容
if (thinking) {
html += `<div class="thinking-block">
<div class="thinking-header" onclick="toggleThinking(this)">
<span><i class="ri-lightbulb-line"></i> 思考过程</span>
<span class="thinking-toggle">点击展开</span>
</div>
<div class="thinking-content">${escapeHtml(thinking)}</div>
</div>`;
}
// 消息内容
html += `<div class="message-content">`;
if (role === 'assistant') {
html += `<div class="markdown-body">${marked.parse(content)}</div>`;
} else {
html += `<div class="user-message-text">${escapeHtml(content)}</div>`;
}
// 复制按钮 - 使用隐藏input存储原始内容
html += `<input type="hidden" class="copy-source" value="${content.replace(/"/g, '&quot;')}">`;
html += `<button class="copy-btn" onclick="copyMessage(this)"><i class="ri-file-copy-line"></i> 复制</button>`;
html += `</div>`;
// Agent信息
if (role === 'assistant' && agentName) {
html += `<div class="agent-info"><i class="ri-robot-line"></i> ${agentName}</div>`;
}
html += '</div>';
div.innerHTML = html;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function toggleThinking(header) {
const block = header.parentElement;
const content = block.querySelector('.thinking-content');
const toggle = header.querySelector('.thinking-toggle');
if (content.classList.contains('expanded')) {
content.classList.remove('expanded');
toggle.textContent = '点击展开';
} else {
content.classList.add('expanded');
toggle.textContent = '点击折叠';
}
}
function copyMessage(btn) {
// 从隐藏input获取原始内容
const hiddenInput = btn.parentElement.querySelector('.copy-source');
if (!hiddenInput) {
console.error('找不到复制源');
return;
}
const text = hiddenInput.value;
// 创建临时textarea复制
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand('copy');
if (success) {
btn.innerHTML = '<i class="ri-check-line"></i> 已复制';
btn.classList.add('copied');
setTimeout(() => {
btn.innerHTML = '<i class="ri-file-copy-line"></i> 复制';
btn.classList.remove('copied');
}, 2000);
} else {
btn.innerHTML = '<i class="ri-error-line"></i> 失败';
}
} catch (err) {
console.error('复制失败:', err);
}
document.body.removeChild(textarea);
}
function escapeHtml(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function displayHistory(messages) {
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
messages.forEach(m => appendMessage(m.role, m.content, m.thinking_content));
}
function clearMessages() {
document.getElementById('messagesContainer').innerHTML = '<div class="welcome"><h2>👋 开始对话</h2></div>';
}
function showError(msg) {
const container = document.getElementById('messagesContainer');
const div = document.createElement('div');
div.className = 'message assistant';
div.innerHTML = `<div class="message-avatar">❌</div><div class="message-body"><div class="message-content" style="color:#dc3545;">${msg}</div></div>`;
container.appendChild(div);
}
// 会话管理
async function loadConversations() {
const res = await fetch('/api/conversations');
const data = await res.json();
const conversations = data.conversations || [];
renderConversations(conversations);
if (!currentConversationId && conversations.length > 0) selectConversation(conversations[0].id);
}
function renderConversations(list) {
const container = document.getElementById('conversationList');
if (list.length === 0) { container.innerHTML = '<div style="text-align:center;color:#999;padding:40px;">暂无对话</div>'; return; }
container.innerHTML = list.map(c => `
<div class="conversation-item ${c.id === currentConversationId ? 'active' : ''}" onclick="selectConversation('${c.id}')">
<span class="title">${c.title || '新对话'}</span>
<button class="delete-btn" onclick="deleteConversation('${c.id}',event)"><i class="ri-delete-bin-line"></i></button>
</div>
`).join('');
}
function selectConversation(id) {
currentConversationId = id;
renderConversations([]);
loadConversations();
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ action: 'select_conversation', conversation_id: id }));
}
async function createNewConversation() {
const res = await fetch('/api/conversations', { method: 'POST' });
const data = await res.json();
currentConversationId = data.id;
clearMessages();
loadConversations();
}
async function deleteConversation(id, e) {
e.stopPropagation();
if (!confirm('确定删除?')) return;
await fetch(`/api/conversations/${id}`, { method: 'DELETE' });
if (id === currentConversationId) { currentConversationId = null; clearMessages(); }
loadConversations();
}
// 发送消息
function sendMessage() {
const input = document.getElementById('messageInput');
const msg = input.value.trim();
if (!msg) return;
document.getElementById('sendBtn').disabled = true;
input.value = '';
input.style.height = 'auto';
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ action: 'chat', message: msg, conversation_id: currentConversationId, agent_id: currentAgentId }));
}
}
function setupTextarea() {
const textarea = document.getElementById('messageInput');
textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
textarea.addEventListener('input', () => { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; });
}
// 快捷语句
function loadQuickPhrases() {
const saved = localStorage.getItem('quickPhrases');
quickPhrases = saved ? JSON.parse(saved) : ['请帮我总结一下', '帮我分析这个问题', '用简单的话解释一下', '给我举个例子', '有什么建议吗?'];
renderQuickPhrases();
}
function renderQuickPhrases() {
const container = document.getElementById('quickPhrasesList');
container.innerHTML = quickPhrases.map((p, i) => `
<span class="phrase-tag" onclick="usePhrase('${escapeHtml(p)}')">${p}<span class="tag-delete" onclick="deletePhrase(${i},event)"><i class="ri-close-line"></i></span></span>
`).join('');
}
function usePhrase(text) {
document.getElementById('messageInput').value = text;
document.getElementById('messageInput').focus();
}
function deletePhrase(index, e) {
e.stopPropagation();
quickPhrases.splice(index, 1);
localStorage.setItem('quickPhrases', JSON.stringify(quickPhrases));
renderQuickPhrases();
}
// 鼠标滚轮横向滚动
function scrollPhrases(e) {
const wrapper = document.getElementById('phraseListWrapper');
wrapper.scrollLeft += e.deltaY;
e.preventDefault();
}
function showAddPhraseModal() {
document.getElementById('phraseModal').classList.add('show');
document.getElementById('newPhraseInput').value = '';
document.getElementById('newPhraseInput').focus();
}
function hidePhraseModal() { document.getElementById('phraseModal').classList.remove('show'); }
function addPhrase() {
const phrase = document.getElementById('newPhraseInput').value.trim();
if (phrase) {
quickPhrases.push(phrase);
localStorage.setItem('quickPhrases', JSON.stringify(quickPhrases));
renderQuickPhrases();
hidePhraseModal();
}
}
document.getElementById('newPhraseInput').addEventListener('keydown', e => { if (e.key === 'Enter') addPhrase(); if (e.key === 'Escape') hidePhraseModal(); });
</script>
</body>
</html>