You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

340 lines
14 KiB

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 聊天</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/@heroicons/react@2.0.18/outline.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#1E40AF',
dark: '#1F2937',
}
}
}
}
</script>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="flex h-screen">
<!-- 侧边栏 -->
<div class="w-64 bg-dark text-white p-4 flex flex-col">
<div class="flex items-center justify-between mb-8">
<h1 class="text-xl font-bold">AI 助手</h1>
<button onclick="newChat()" class="p-2 hover:bg-gray-700 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
<!-- 会话列表 -->
<div id="chat-list" class="flex-1 overflow-y-auto space-y-2">
<!-- 会话将在这里动态添加 -->
</div>
<!-- 底部状态 -->
<div class="mt-4 pt-4 border-t border-gray-700">
<div id="status" class="text-sm text-gray-400">正在连接...</div>
</div>
</div>
<!-- 主聊天区域 -->
<div class="flex-1 flex flex-col">
<!-- 聊天头部 -->
<div class="bg-white border-b p-4 flex items-center justify-between">
<h2 id="current-chat-title" class="text-lg font-semibold text-gray-800">新会话</h2>
<div class="flex items-center space-x-4">
<!-- RAG 开关 -->
<div class="flex items-center space-x-2">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="use-rag" class="sr-only peer" checked>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
<span class="ml-2 text-sm font-medium text-gray-700">RAG</span>
</label>
</div>
<button onclick="clearChat()" class="p-2 text-gray-600 hover:bg-gray-100 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<!-- 聊天容器 -->
<div id="chat-container" class="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
<!-- 消息将在这里动态添加 -->
</div>
<!-- 输入区域 -->
<div class="border-t border-gray-200 p-4 bg-white">
<div class="flex space-x-4">
<div class="flex-1 relative">
<textarea
id="message-input"
class="w-full rounded-lg border border-gray-300 px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
placeholder="输入您的问题..."
rows="1"
onkeydown="handleKeyPress(event)"
></textarea>
<div class="absolute right-2 bottom-2 text-sm text-gray-500">
<span id="char-count">0</span>/1000
</div>
</div>
<button
onclick="sendMessage()"
class="bg-primary hover:bg-secondary text-white font-medium px-6 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2"
>
<span>发送</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
</svg>
</button>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let ws = null;
let currentChatId = null;
let chats = new Map();
// 初始化
function init() {
connect();
loadChats();
setupMessageInput();
}
// WebSocket 连接
function connect() {
ws = new WebSocket(`ws://${window.location.host}/ws`);
ws.onopen = () => {
updateStatus('已连接', 'text-green-400');
};
ws.onclose = () => {
updateStatus('已断开连接', 'text-red-400');
setTimeout(connect, 3000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
updateStatus('连接错误', 'text-red-400');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
addMessage('AI', data.answer, 'ai');
saveChat(currentChatId);
};
}
// 更新状态显示
function updateStatus(text, colorClass) {
const statusDiv = document.getElementById('status');
statusDiv.textContent = text;
statusDiv.className = `text-sm ${colorClass}`;
}
// 发送消息
function sendMessage() {
const messageInput = document.getElementById('message-input');
const message = messageInput.value.trim();
const useRag = document.getElementById('use-rag').checked;
if (message && ws?.readyState === WebSocket.OPEN) {
if (!currentChatId) {
currentChatId = Date.now().toString();
createNewChat(currentChatId, message);
}
ws.send(JSON.stringify({
message,
chatId: currentChatId,
use_rag: useRag
}));
addMessage('我', message, 'user');
messageInput.value = '';
updateCharCount();
saveChat(currentChatId);
}
}
// 添加消息到聊天界面
function addMessage(sender, text, type) {
const chatContainer = document.getElementById('chat-container');
const messageDiv = document.createElement('div');
messageDiv.className = `flex ${type === 'user' ? 'justify-end' : 'justify-start'}`;
const messageContent = document.createElement('div');
messageContent.className = `max-w-[70%] rounded-lg px-4 py-2 ${
type === 'user'
? 'bg-primary text-white rounded-br-none'
: 'bg-white text-gray-800 rounded-bl-none shadow-sm'
}`;
const senderSpan = document.createElement('div');
senderSpan.className = 'text-xs font-medium mb-1';
senderSpan.textContent = sender;
const textDiv = document.createElement('div');
textDiv.className = 'text-sm whitespace-pre-wrap';
textDiv.textContent = text;
messageContent.appendChild(senderSpan);
messageContent.appendChild(textDiv);
messageDiv.appendChild(messageContent);
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// 创建新会话
function newChat() {
currentChatId = null;
document.getElementById('chat-container').innerHTML = '';
document.getElementById('current-chat-title').textContent = '新会话';
document.getElementById('message-input').value = '';
updateCharCount();
}
// 清除当前会话
function clearChat() {
if (currentChatId) {
chats.delete(currentChatId);
saveChats();
newChat();
}
}
// 创建新会话项
function createNewChat(id, firstMessage) {
const chatList = document.getElementById('chat-list');
const chatItem = document.createElement('div');
chatItem.className = 'p-2 hover:bg-gray-700 rounded-lg cursor-pointer';
chatItem.onclick = () => loadChat(id);
const title = firstMessage.length > 20 ? firstMessage.substring(0, 20) + '...' : firstMessage;
chatItem.innerHTML = `
<div class="text-sm font-medium">${title}</div>
<div class="text-xs text-gray-400">${new Date().toLocaleString()}</div>
`;
chatList.insertBefore(chatItem, chatList.firstChild);
chats.set(id, {
title: title,
messages: [],
timestamp: Date.now()
});
document.getElementById('current-chat-title').textContent = title;
}
// 加载会话
function loadChat(id) {
const chat = chats.get(id);
if (chat) {
currentChatId = id;
document.getElementById('chat-container').innerHTML = '';
document.getElementById('current-chat-title').textContent = chat.title;
chat.messages.forEach(msg => {
addMessage(msg.sender, msg.text, msg.type);
});
}
}
// 保存会话
function saveChat(id) {
const chat = chats.get(id);
if (chat) {
const messages = [];
document.querySelectorAll('#chat-container > div').forEach(div => {
const content = div.querySelector('div:last-child');
const sender = div.querySelector('div:first-child').textContent;
const type = div.classList.contains('justify-end') ? 'user' : 'ai';
messages.push({
sender,
text: content.textContent,
type
});
});
chat.messages = messages;
saveChats();
}
}
// 保存所有会话到本地存储
function saveChats() {
localStorage.setItem('chats', JSON.stringify(Array.from(chats.entries())));
}
// 从本地存储加载会话
function loadChats() {
const savedChats = localStorage.getItem('chats');
if (savedChats) {
chats = new Map(JSON.parse(savedChats));
const chatList = document.getElementById('chat-list');
chatList.innerHTML = '';
Array.from(chats.entries())
.sort((a, b) => b[1].timestamp - a[1].timestamp)
.forEach(([id, chat]) => {
const chatItem = document.createElement('div');
chatItem.className = 'p-2 hover:bg-gray-700 rounded-lg cursor-pointer';
chatItem.onclick = () => loadChat(id);
chatItem.innerHTML = `
<div class="text-sm font-medium">${chat.title}</div>
<div class="text-xs text-gray-400">${new Date(chat.timestamp).toLocaleString()}</div>
`;
chatList.appendChild(chatItem);
});
}
}
// 设置消息输入框
function setupMessageInput() {
const messageInput = document.getElementById('message-input');
messageInput.addEventListener('input', updateCharCount);
}
// 更新字符计数
function updateCharCount() {
const messageInput = document.getElementById('message-input');
const charCount = document.getElementById('char-count');
const count = messageInput.value.length;
charCount.textContent = count;
if (count > 1000) {
charCount.className = 'text-red-500';
} else {
charCount.className = 'text-gray-500';
}
}
// 处理按键事件
function handleKeyPress(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// 初始化应用
init();
</script>
</body>
</html>