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

590 lines
17 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 对话系统</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, 'Helvetica Neue', Arial, 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;
}
.user-id-display {
font-size: 12px;
color: #666;
background: #f0f0f0;
padding: 4px 8px;
border-radius: 4px;
}
.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;
}
.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;
}
.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 {
display: flex;
align-items: center;
gap: 8px;
color: #666;
}
.loading::after {
content: '';
animation: dots 1.5s infinite;
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
.empty-state {
text-align: center;
color: #666;
padding: 40px;
}
/* 滚动条样式 */
::-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">暂无对话</div>
</div>
</div>
<div class="main-content">
<div class="chat-header">
<h1>AI 对话</h1>
<div class="user-id-display" id="userIdDisplay">用户: 加载中...</div>
</div>
<div class="messages-container" id="messagesContainer">
<div class="welcome">
<h2>👋 欢迎使用 AI 对话系统</h2>
<p>开始一段新对话吧</p>
</div>
</div>
<div class="input-container">
<div class="input-wrapper">
<textarea
id="messageInput"
placeholder="输入消息... (Shift+Enter换行, Enter发送)"
rows="1"
></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()">
<i class="ri-send-plane-fill"></i>
</button>
</div>
</div>
</div>
<script>
// 全局变量
let ws = null;
let userId = 'web_' + Math.random().toString(36).substr(2, 9);
let currentConversationId = null;
let conversations = [];
// 初始化
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('userIdDisplay').textContent = `用户: ${userId}`;
connectWebSocket();
loadConversations();
setupTextarea();
});
// 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) => {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
};
ws.onclose = () => {
console.log('WebSocket已断开3秒后重连...');
setTimeout(connectWebSocket, 3000);
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
}
// 处理WebSocket消息
function handleWebSocketMessage(data) {
switch (data.type) {
case 'history':
displayHistory(data.messages);
break;
case 'conversation_created':
currentConversationId = data.conversation_id;
loadConversations();
break;
case 'user_message':
appendMessage('user', data.message.content, data.message.source);
break;
case 'assistant_message':
appendMessage('assistant', data.message.content, data.message.source);
document.getElementById('sendBtn').disabled = false;
break;
case 'error':
alert(data.message);
document.getElementById('sendBtn').disabled = false;
break;
}
}
// 显示历史消息
function displayHistory(messages) {
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
messages.forEach(msg => {
appendMessage(msg.role, msg.content, msg.source);
});
}
// 添加消息到界面
function appendMessage(role, content, source = 'web') {
const container = document.getElementById('messagesContainer');
// 移除欢迎界面
const welcome = container.querySelector('.welcome');
if (welcome) welcome.remove();
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const avatar = role === 'user' ? '👤' : '🤖';
const sourceLabel = source === 'matrix' ? ' [Matrix]' : '';
messageDiv.innerHTML = `
<div class="message-avatar">${avatar}</div>
<div class="message-content">${formatContent(content)}${sourceLabel}</div>
`;
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
}
// 格式化消息内容简单的Markdown支持
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 response = await fetch('/api/conversations?user_id=' + userId);
const data = await response.json();
conversations = data.conversations;
renderConversations();
} catch (error) {
console.error('加载会话失败:', error);
}
}
// 渲染会话列表
function renderConversations() {
const container = document.getElementById('conversationList');
if (conversations.length === 0) {
container.innerHTML = '<div class="empty-state">暂无对话</div>';
return;
}
container.innerHTML = conversations.map(conv => `
<div class="conversation-item ${conv.id === currentConversationId ? 'active' : ''}"
onclick="selectConversation('${conv.id}')">
<span class="title">${conv.title}</span>
<button class="delete-btn" onclick="deleteConversation('${conv.id}', event)">
<i class="ri-delete-bin-line"></i>
</button>
</div>
`).join('');
}
// 选择会话
function selectConversation(conversationId) {
currentConversationId = conversationId;
renderConversations();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
action: 'select_conversation',
conversation_id: conversationId
}));
}
}
// 创建新会话
async function createNewConversation() {
try {
const response = await fetch('/api/conversations?user_id=' + userId, {
method: 'POST'
});
const data = await response.json();
currentConversationId = data.id;
// 清空消息区域
const container = document.getElementById('messagesContainer');
container.innerHTML = `
<div class="welcome">
<h2>👋 开始新对话</h2>
<p>输入您的问题开始对话</p>
</div>
`;
loadConversations();
} catch (error) {
console.error('创建会话失败:', error);
}
}
// 删除会话
async function deleteConversation(conversationId, event) {
event.stopPropagation();
if (!confirm('确定删除这个对话?')) return;
try {
await fetch(`/api/conversations/${conversationId}`, {
method: 'DELETE'
});
if (conversationId === currentConversationId) {
currentConversationId = null;
const container = document.getElementById('messagesContainer');
container.innerHTML = `
<div class="welcome">
<h2>👋 欢迎使用 AI 对话系统</h2>
<p>开始一段新对话吧</p>
</div>
`;
}
loadConversations();
} catch (error) {
console.error('删除会话失败:', error);
}
}
// 发送消息
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (!message) return;
// 禁用发送按钮
document.getElementById('sendBtn').disabled = true;
// 清空输入框
input.value = '';
input.style.height = 'auto';
// 发送WebSocket消息
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
action: 'chat',
message: message,
conversation_id: currentConversationId
}));
}
}
// 设置文本框
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';
});
}
</script>
</body>
</html>