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

616 lines
28 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">
<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选择器 */
.agent-selector { display: flex; align-items: center; gap: 8px; }
.agent-selector label { font-size: 14px; color: #666; }
.agent-selector select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; background: #fff; cursor: pointer; min-width: 150px; }
.agent-selector select:focus { border-color: #10a37f; outline: none; }
.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-content { flex: 1; line-height: 1.6; }
.message-content pre { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 12px 0; }
.message-content code { background: #f0f0f0; padding: 2px 6px; border-radius: 4px; font-family: monospace; }
.message-content pre code { background: transparent; padding: 0; }
/* 思考内容样式 */
.thinking-content { background: #f8f9fa; border-left: 3px solid #667eea; padding: 12px 16px; margin: 8px 0; font-size: 14px; color: #666; border-radius: 4px; }
.thinking-toggle { cursor: pointer; color: #667eea; font-size: 12px; margin-top: 4px; }
.input-container { padding: 24px; border-top: 1px solid #e5e5e5; }
.input-wrapper { max-width: 800px; margin: 0 auto; display: flex; gap: 12px; align-items: flex-end; }
.input-wrapper textarea { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 12px; font-size: 16px; resize: none; outline: none; font-family: inherit; max-height: 200px; }
.input-wrapper 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 { max-width: 800px; margin: 0 auto; padding-top: 12px; }
.quick-phrases-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.quick-phrases-header span { font-size: 12px; color: #999; }
.quick-phrases-header button { font-size: 12px; background: none; border: 1px solid #ddd; padding: 4px 8px; border-radius: 4px; cursor: pointer; color: #666; }
.quick-phrases-header button:hover { background: #f5f5f5; }
.quick-phrases-list { display: flex; flex-wrap: wrap; gap: 8px; }
.quick-phrase-item { display: flex; align-items: center; gap: 4px; padding: 6px 12px; background: #f0f0f0; border-radius: 16px; cursor: pointer; font-size: 13px; color: #333; transition: all 0.2s; border: 1px solid transparent; }
.quick-phrase-item:hover { background: #e8e8e8; border-color: #10a37f; }
.quick-phrase-item .delete-btn { opacity: 0; font-size: 12px; color: #999; margin-left: 4px; }
.quick-phrase-item:hover .delete-btn { opacity: 1; }
/* 快捷语句管理弹窗 */
.phrase-modal { 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; }
.phrase-modal.show { display: flex; }
.phrase-modal-content { background: #fff; padding: 24px; border-radius: 12px; max-width: 400px; width: 90%; }
.phrase-modal-content h3 { margin-bottom: 16px; }
.phrase-modal-content input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 16px; }
.phrase-modal-buttons { display: flex; gap: 12px; justify-content: flex-end; }
.phrase-modal-buttons button { padding: 8px 16px; border-radius: 8px; cursor: pointer; }
.phrase-modal-buttons .cancel { background: #f5f5f5; border: 1px solid #ddd; }
.phrase-modal-buttons .save { background: #10a37f; color: #fff; border: none; }
.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; }
.welcome p { font-size: 16px; }
.loading::after { content: ''; animation: dots 1.5s infinite; }
@keyframes dots { 0%, 20% { content: '.'; } 40% { content: '..'; } 60%, 100% { content: '...'; } }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a1a1a1; }
</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 class="empty-state" style="text-align:center;color:#999;padding:40px;">暂无对话</div>
</div>
</div>
<div class="main-content">
<div class="chat-header">
<h1>AI 对话 v2.0</h1>
<div class="header-controls">
<div class="agent-selector">
<label><i class="ri-robot-line"></i> Agent:</label>
<select id="agentSelect" onchange="switchAgent()">
<option value="">加载中...</option>
</select>
</div>
<div style="font-size:12px;color:#666;background:#f0f0f0;padding:4px 8px;border-radius:4px;">
<i class="ri-user-line"></i> 主用户
</div>
</div>
</div>
<div class="messages-container" id="messagesContainer">
<div class="welcome">
<h2>👋 欢迎使用 AI 对话系统</h2>
<p>选择Agent开始对话吧</p>
</div>
</div>
<div class="input-container">
<div class="input-wrapper">
<textarea id="messageInput" placeholder="输入消息... (Shift+Enter换行)" rows="1"></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()">
<i class="ri-send-plane-fill"></i>
</button>
</div>
<!-- 快捷语句 -->
<div class="quick-phrases">
<div class="quick-phrases-header">
<span><i class="ri-flashlight-line"></i> 快捷语句</span>
<button onclick="showAddPhraseModal()"><i class="ri-add-line"></i> 添加</button>
</div>
<div class="quick-phrases-list" id="quickPhrasesList"></div>
</div>
</div>
</div>
<!-- 添加快捷语句弹窗 -->
<div class="phrase-modal" id="phraseModal">
<div class="phrase-modal-content">
<h3><i class="ri-add-line"></i> 添加快捷语句</h3>
<input type="text" id="newPhraseInput" placeholder="输入快捷语句内容..." maxlength="100">
<div class="phrase-modal-buttons">
<button class="cancel" onclick="hidePhraseModal()">取消</button>
<button class="save" onclick="addPhrase()">添加</button>
</div>
</div>
</div>
<script>
// 全局变量
let ws = null;
let userId = 'main_user';
let currentConversationId = null;
let currentAgentId = null;
let conversations = [];
let agents = [];
let quickPhrases = [];
// 默认快捷语句
const DEFAULT_phrases = [
'请帮我总结一下',
'帮我分析这个问题',
'用简单的话解释一下',
'给我举个例子',
'有什么建议吗?'
];
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadAgents();
loadQuickPhrases();
connectWebSocket();
loadConversations();
setupTextarea();
setInterval(checkLatestConversation, 3000);
});
// 加载Agent列表
async function loadAgents() {
try {
const res = await fetch('/api/v2/agents');
const data = await res.json();
agents = data.agents || [];
renderAgentSelect();
// 设置当前Agent
const defaultAgent = agents.find(a => a.is_default) || agents[0];
if (defaultAgent) currentAgentId = defaultAgent.id;
} catch (e) {
console.error('加载Agent失败:', e);
document.getElementById('agentSelect').innerHTML = '<option value="">加载失败</option>';
}
}
// 渲染Agent下拉框
function renderAgentSelect(selectedId = null) {
const select = document.getElementById('agentSelect');
const selected = selectedId || currentAgentId;
select.innerHTML = agents.filter(a => a.is_active).map(a =>
`<option value="${a.id}" ${a.id === selected ? 'selected' : ''}>${a.display_name || a.name}</option>`
).join('');
}
// 更新Agent选择框显示
function updateAgentSelect(agentId) {
const select = document.getElementById('agentSelect');
select.value = agentId;
currentAgentId = agentId;
}
// 切换Agent - 自动创建新对话
async function switchAgent() {
const newAgentId = document.getElementById('agentSelect').value;
if (newAgentId && parseInt(newAgentId) !== currentAgentId) {
const oldAgentId = currentAgentId;
currentAgentId = parseInt(newAgentId);
console.log('切换Agent:', currentAgentId);
// 发送切换消息到WebSocket
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
action: 'switch_agent',
agent_id: currentAgentId
}));
}
// 自动创建新对话
await createNewConversation();
// 显示切换提示
const agent = agents.find(a => a.id === currentAgentId);
if (agent) {
showAgentSwitchNotice(agent.display_name || agent.name);
}
}
}
// 显示Agent切换提示
function showAgentSwitchNotice(agentName) {
const container = document.getElementById('messagesContainer');
// 清空现有内容,显示切换提示
container.innerHTML = `
<div class="welcome">
<h2>🔄 已切换 Agent</h2>
<p style="color:#667eea;font-size:18px;"><strong>${agentName}</strong></p>
<p style="color:#999;font-size:14px;">已为您创建新对话,可直接开始聊天</p>
</div>
`;
}
// 加载快捷语句
function loadQuickPhrases() {
const saved = localStorage.getItem('quickPhrases');
quickPhrases = saved ? JSON.parse(saved) : DEFAULT_phrrases;
renderQuickPhrases();
}
// 渲染快捷语句
function renderQuickPhrases() {
const container = document.getElementById('quickPhrasesList');
if (quickPhrases.length === 0) {
container.innerHTML = '<span style="color:#999;font-size:12px;">点击"添加"创建快捷语句</span>';
return;
}
container.innerHTML = quickPhrases.map((p, i) => `
<div class="quick-phrase-item" onclick="usePhrase(${i})">
<span>${p}</span>
<span class="delete-btn" onclick="deletePhrase(${i}, event)"><i class="ri-close-line"></i></span>
</div>
`).join('');
}
// 使用快捷语句
function usePhrase(index) {
const input = document.getElementById('messageInput');
input.value = quickPhrases[index];
input.focus();
}
// 删除快捷语句
function deletePhrase(index, event) {
event.stopPropagation();
quickPhrases.splice(index, 1);
localStorage.setItem('quickPhrases', JSON.stringify(quickPhrases));
renderQuickPhrases();
}
// 显示添加弹窗
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 input = document.getElementById('newPhraseInput');
const phrase = input.value.trim();
if (phrase) {
quickPhrases.push(phrase);
localStorage.setItem('quickPhrases', JSON.stringify(quickPhrases));
renderQuickPhrases();
hidePhraseModal();
}
}
// 检查最新会话
async function checkLatestConversation() {
try {
const res = await fetch('/api/conversations/latest');
if (res.ok) {
const data = await res.json();
if (data.conversation && data.conversation.id !== currentConversationId) {
selectConversation(data.conversation.id);
}
}
} catch (e) {}
}
// WebSocket连接
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws/${userId}`);
ws.onopen = () => console.log('WebSocket已连接');
ws.onmessage = (event) => handleWebSocketMessage(JSON.parse(event.data));
ws.onclose = () => setTimeout(connectWebSocket, 3000);
ws.onerror = (e) => console.error('WebSocket错误:', e);
}
// 处理WebSocket消息
function handleWebSocketMessage(data) {
switch (data.type) {
case 'history':
displayHistory(data.messages);
// 更新Agent选择框为该对话使用的Agent
if (data.agent_id) {
currentAgentId = 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;
console.log('Agent已切换:', data.agent_name);
break;
case 'thinking_start':
showThinkingIndicator();
break;
case 'thinking_content':
updateThinkingContent(data.content);
break;
case 'thinking_end':
hideThinkingIndicator();
break;
case 'user_message':
appendMessage('user', data.message.content, data.message.source);
if (data.message.source === 'matrix' && data.conversation_id) {
currentConversationId = data.conversation_id;
renderConversations();
}
break;
case 'assistant_message':
appendMessage('assistant', data.message.content, data.message.source, data.message.thinking_content, data.message.agent_name);
document.getElementById('sendBtn').disabled = false;
break;
case 'error':
alert(data.message);
document.getElementById('sendBtn').disabled = false;
break;
}
}
// 显示思考指示器
function showThinkingIndicator() {
const container = document.getElementById('messagesContainer');
const thinking = document.createElement('div');
thinking.id = 'thinkingIndicator';
thinking.className = 'message assistant';
thinking.innerHTML = `
<div class="message-avatar">🤔</div>
<div class="message-content" style="color:#667eea;">
<span class="loading">正在思考</span>
</div>
`;
container.appendChild(thinking);
container.scrollTop = container.scrollHeight;
}
// 更新思考内容
function updateThinkingContent(content) {
const indicator = document.getElementById('thinkingIndicator');
if (indicator) {
indicator.querySelector('.message-content').innerHTML = `
<div class="thinking-content">${formatContent(content)}</div>
`;
}
}
// 隐藏思考指示器
function hideThinkingIndicator() {
const indicator = document.getElementById('thinkingIndicator');
if (indicator) indicator.remove();
}
// 清空消息
function clearMessages() {
document.getElementById('messagesContainer').innerHTML = `
<div class="welcome">
<h2>👋 开始新对话</h2>
<p>当前Agent: ${getAgentName()}</p>
</div>
`;
}
// 获取当前Agent名称
function getAgentName() {
const agent = agents.find(a => a.id === currentAgentId);
return agent ? (agent.display_name || agent.name) : '默认助手';
}
// 显示历史消息
function displayHistory(messages) {
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
messages.forEach(msg => {
appendMessage(msg.role, msg.content, msg.source, msg.thinking_content);
});
}
// 添加消息
function appendMessage(role, content, source = 'web', thinking = null, agentName = null) {
const container = document.getElementById('messagesContainer');
container.querySelector('.welcome')?.remove();
document.getElementById('thinkingIndicator')?.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-content">`;
// 思考内容
if (thinking) {
html += `<div class="thinking-content" id="thinking-${Date.now()}">
<strong>💭 思考过程:</strong><br>${formatContent(thinking)}
<div class="thinking-toggle" onclick="toggleThinking(this)">收起</div>
</div>`;
}
// 主要内容
html += formatContent(content);
// Agent信息
if (role === 'assistant' && agentName) {
html += `<div style="font-size:12px;color:#999;margin-top:8px;"><i class="ri-robot-line"></i> ${agentName}</div>`;
}
html += '</div>';
div.innerHTML = html;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
// 切换思考内容显示
function toggleThinking(btn) {
const content = btn.parentElement;
const isCollapsed = content.style.maxHeight;
if (isCollapsed) {
content.style.maxHeight = '';
content.style.overflow = '';
btn.textContent = '收起';
} else {
content.style.maxHeight = '60px';
content.style.overflow = 'hidden';
btn.textContent = '展开';
}
}
// 格式化内容
function formatContent(content) {
return content
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\n/g, '<br>');
}
// 加载会话列表
async function loadConversations() {
try {
const res = await fetch('/api/conversations');
const data = await res.json();
conversations = data.conversations || [];
renderConversations();
if (!currentConversationId && conversations.length > 0) {
selectConversation(conversations[0].id);
}
} catch (e) { console.error('加载会话失败:', e); }
}
// 渲染会话列表
function renderConversations() {
const container = document.getElementById('conversationList');
if (conversations.length === 0) {
container.innerHTML = '<div style="text-align:center;color:#999;padding:40px;">暂无对话</div>';
return;
}
container.innerHTML = conversations.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();
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ action: 'select_conversation', conversation_id: id }));
}
}
// 创建新会话
async function createNewConversation() {
try {
const res = await fetch('/api/conversations', { method: 'POST' });
const data = await res.json();
currentConversationId = data.id;
clearMessages();
loadConversations();
} catch (e) { console.error('创建会话失败:', e); }
}
// 删除会话
async function deleteConversation(id, event) {
event.stopPropagation();
if (!confirm('确定删除?')) return;
try {
await fetch(`/api/conversations/${id}`, { method: 'DELETE' });
if (id === currentConversationId) {
currentConversationId = null;
clearMessages();
}
loadConversations();
} catch (e) { console.error('删除失败:', e); }
}
// 发送消息
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';
});
}
// 弹窗键盘事件
document.getElementById('newPhraseInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') addPhrase();
if (e.key === 'Escape') hidePhraseModal();
});
</script>
</body>
</html>