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
340 lines
14 KiB
1 month ago
|
<!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>
|