Files
hustoj/web/llm-chat.php

516 lines
16 KiB
PHP
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.
<?php
require_once('./include/db_info.inc.php');
require_once('./include/setlang.php');
require_once('./include/const.inc.php');
$show_title = "AI 聊天 - " . $OJ_NAME;
$OJ_CACHE_SHARE = false;
require_once('./template/' . $OJ_TEMPLATE . '/header.php');
?>
<style>
body { overflow: hidden !important; }
.chat-wrapper {
display: flex;
height: calc(100vh - 55px);
margin-top: 5px;
}
/* 左侧对话列表 */
.chat-sidebar {
width: 260px;
min-width: 260px;
background: #1a1a2e;
color: #e0e0e0;
display: flex;
flex-direction: column;
border-right: 1px solid #2a2a4a;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid #2a2a4a;
}
.sidebar-header button {
width: 100%;
padding: 10px;
background: #2d2d5e;
color: #fff;
border: 1px solid #4a4a8a;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.sidebar-header button:hover { background: #3d3d7e; }
.session-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.session-item {
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 4px;
display: flex;
align-items: center;
justify-content: space-between;
transition: background 0.2s;
font-size: 14px;
color: #ccc;
}
.session-item:hover { background: #2a2a4a; }
.session-item.active { background: #2d2d5e; color: #fff; }
.session-item .session-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-item .session-actions {
display: none;
gap: 4px;
}
.session-item:hover .session-actions { display: flex; }
.session-actions button {
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 2px 4px;
font-size: 12px;
border-radius: 4px;
}
.session-actions button:hover { color: #fff; background: #444; }
.session-actions button.delete-btn:hover { color: #ff6b6b; }
/* 右侧聊天区 */
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
.chat-header {
padding: 14px 20px;
background: #fff;
border-bottom: 1px solid #e8e8e8;
display: flex;
align-items: center;
gap: 12px;
}
.chat-header .ai-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 16px;
}
.chat-header .ai-info h3 { margin: 0; font-size: 16px; color: #333; }
.chat-header .ai-info p { margin: 0; font-size: 12px; color: #999; }
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
display: flex;
margin-bottom: 16px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.message.user { justify-content: flex-end; }
.message.assistant { justify-content: flex-start; }
.msg-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
font-weight: bold;
}
.message.user .msg-avatar {
background: #0084ff;
color: #fff;
margin-left: 10px;
order: 2;
}
.message.assistant .msg-avatar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
margin-right: 10px;
}
.msg-content {
max-width: 70%;
padding: 10px 14px;
border-radius: 16px;
line-height: 1.6;
font-size: 15px;
}
.message.user .msg-content {
background: #0084ff;
color: #fff;
border-bottom-right-radius: 4px;
}
.message.assistant .msg-content {
background: #fff;
color: #333;
border: 1px solid #e8e8e8;
border-bottom-left-radius: 4px;
}
/* Markdown 样式 */
.msg-content h1, .msg-content h2, .msg-content h3 { margin: 8px 0 4px; color: #222; }
.msg-content p { margin: 4px 0; }
.msg-content code { background: #f4f4f4; padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }
.msg-content pre { background: #282c34; color: #abb2bf; padding: 12px; border-radius: 8px; overflow-x: auto; margin: 8px 0; }
.msg-content pre code { background: none; color: inherit; }
.msg-content ul, .msg-content ol { margin: 4px 0; padding-left: 20px; }
/* 输入区 */
.chat-input-area {
padding: 16px 20px;
background: #fff;
border-top: 1px solid #e8e8e8;
display: flex;
gap: 10px;
}
.chat-input-area textarea {
flex: 1;
padding: 10px 14px;
border: 2px solid #e8e8e8;
border-radius: 10px;
font-size: 15px;
resize: none;
outline: none;
min-height: 42px;
max-height: 100px;
transition: border-color 0.2s;
}
.chat-input-area textarea:focus { border-color: #0084ff; }
.chat-input-area button {
padding: 10px 20px;
background: #0084ff;
color: #fff;
border: none;
border-radius: 10px;
font-size: 15px;
cursor: pointer;
transition: background 0.2s;
}
.chat-input-area button:hover { background: #0073e6; }
.chat-input-area button:disabled { background: #ccc; cursor: not-allowed; }
.typing-indicator { display: flex; padding: 4px 0; }
.typing-indicator span { width: 7px; height: 7px; background: #999; border-radius: 50%; margin: 0 2px; animation: typing 1.4s infinite; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing { 0%,100% { opacity: 0.2; } 50% { opacity: 1; } }
.welcome-screen {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
}
.welcome-screen .big-icon { font-size: 72px; margin-bottom: 16px; }
.welcome-screen h2 { color: #555; margin-bottom: 8px; }
.welcome-screen p { font-size: 15px; }
/* 移动端适配 */
@media (max-width: 768px) {
.chat-sidebar { width: 0; min-width: 0; display: none; }
}
</style>
<div class="chat-wrapper">
<!-- 左侧对话列表 -->
<div class="chat-sidebar">
<div class="sidebar-header">
<button onclick="createSession()"><i class="plus icon"></i> 新建对话</button>
</div>
<div class="session-list" id="sessionList"></div>
</div>
<!-- 右侧聊天区 -->
<div class="chat-main">
<div class="chat-header">
<div class="ai-avatar">M</div>
<div class="ai-info">
<h3>MiniMax老师</h3>
<p>AI助教 · 随时为你解答</p>
</div>
</div>
<div id="chatMessages" class="chat-messages">
<div class="welcome-screen" id="welcomeScreen">
<i class="comment outline icon big-icon"></i>
<h2>你好欢迎使用AI聊天</h2>
<p>选择一个对话或创建新对话开始聊天</p>
</div>
</div>
<div class="chat-input-area">
<textarea id="userInput" placeholder="输入你的问题... (Enter发送Shift+Enter换行)" rows="1"></textarea>
<button id="sendBtn" onclick="sendMessage()"><i class="send icon"></i></button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
let currentSessionId = 0;
let isLoading = false;
const userInput = document.getElementById('userInput');
userInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 100) + 'px'; });
userInput.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
// 加载会话列表
async function loadSessions() {
try {
const res = await fetch('llm-chat-api.php?action=sessions');
const data = await res.json();
const list = document.getElementById('sessionList');
const sessions = data.sessions || [];
if (sessions.length === 0) {
list.innerHTML = '<div style="text-align:center;color:#666;padding:20px;font-size:13px;">暂无对话</div>';
return;
}
list.innerHTML = sessions.map(s => `
<div class="session-item ${s.session_id == currentSessionId ? 'active' : ''}"
onclick="switchSession(${s.session_id})" data-id="${s.session_id}">
<span class="session-title">${escapeHtml(s.title)}</span>
<span class="session-actions">
<button onclick="event.stopPropagation();renameSession(${s.session_id})" title="重命名">✏️</button>
<button class="delete-btn" onclick="event.stopPropagation();deleteSession(${s.session_id})" title="删除">🗑️</button>
</span>
</div>
`).join('');
} catch (e) { console.error('加载会话列表失败', e); }
}
// 创建新会话
async function createSession() {
try {
const res = await fetch('llm-chat-api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'create_session', title: '新对话' })
});
const data = await res.json();
if (data.session_id) {
currentSessionId = data.session_id;
localStorage.setItem('llm_chat_current', currentSessionId);
await loadSessions();
clearChatArea();
}
} catch (e) { alert('创建失败'); }
}
// 切换会话
async function switchSession(sessionId) {
if (sessionId == currentSessionId) return;
currentSessionId = sessionId;
localStorage.setItem('llm_chat_current', currentSessionId);
await loadSessions();
await loadMessages(sessionId);
}
// 加载消息
async function loadMessages(sessionId) {
try {
const res = await fetch(`llm-chat-api.php?action=messages&session_id=${sessionId}`);
const data = await res.json();
const container = document.getElementById('chatMessages');
container.innerHTML = '';
const messages = data.messages || [];
if (messages.length === 0) {
container.innerHTML = '<div class="welcome-screen"><i class="comment outline icon big-icon"></i><h2>开始对话吧</h2><p>输入你想问的问题</p></div>';
return;
}
messages.forEach(msg => {
addMessageToUI(msg.role, msg.content);
});
scrollToBottom();
} catch (e) { console.error('加载消息失败', e); }
}
// 删除会话
async function deleteSession(sessionId) {
if (!confirm('确定要删除这个对话吗?')) return;
try {
await fetch('llm-chat-api.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
if (sessionId == currentSessionId) {
currentSessionId = 0;
localStorage.removeItem('llm_chat_current');
clearChatArea();
}
await loadSessions();
} catch (e) { alert('删除失败'); }
}
// 重命名会话
async function renameSession(sessionId) {
const title = prompt('输入新名称:');
if (!title || !title.trim()) return;
try {
await fetch('llm-chat-api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'rename_session', session_id: sessionId, title: title.trim() })
});
await loadSessions();
} catch (e) { alert('重命名失败'); }
}
// 清空聊天区
function clearChatArea() {
document.getElementById('chatMessages').innerHTML = `
<div class="welcome-screen" id="welcomeScreen">
<i class="comment outline icon big-icon"></i>
<h2>开始对话吧</h2>
<p>输入你想问的问题</p>
</div>
`;
}
// 发送消息
async function sendMessage() {
const input = document.getElementById('userInput');
const message = input.value.trim();
if (!message || isLoading) return;
input.value = '';
input.style.height = 'auto';
// 清空欢迎页
const welcome = document.getElementById('welcomeScreen');
if (welcome) welcome.remove();
addMessageToUI('user', message);
isLoading = true;
document.getElementById('sendBtn').disabled = true;
const assistantDiv = addMessageToUI('assistant', '', true);
try {
const res = await fetch('llm-chat-api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'chat', message: message, session_id: currentSessionId })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.text) {
fullText += data.text;
assistantDiv.innerHTML = renderMarkdown(fullText);
scrollToBottom();
}
if (data.session_id && !currentSessionId) {
currentSessionId = data.session_id;
localStorage.setItem('llm_chat_current', currentSessionId);
loadSessions();
}
} catch (e) {}
}
}
}
} catch (error) {
assistantDiv.innerHTML = '<span style="color:#ff4444">抱歉,发生错误: ' + error.message + '</span>';
} finally {
isLoading = false;
document.getElementById('sendBtn').disabled = false;
// 如果是新会话,刷新列表
loadSessions();
}
}
// 添加消息到UI
function addMessageToUI(role, content, loading = false) {
const container = document.getElementById('chatMessages');
const div = document.createElement('div');
div.className = 'message ' + role;
if (role === 'user') {
const userInitial = '<?php echo isset($_SESSION[$OJ_NAME . "_user_id"]) ? mb_substr($_SESSION[$OJ_NAME . "_user_id"], 0, 1, "UTF-8") : "U"; ?>';
div.innerHTML = `<div class="msg-content">${escapeHtml(content)}</div><div class="msg-avatar">${userInitial}</div>`;
} else {
div.innerHTML = `<div class="msg-avatar">M</div><div class="msg-content">${loading ? '<div class="typing-indicator"><span></span><span></span><span></span></div>' : renderMarkdown(content)}</div>`;
}
container.appendChild(div);
scrollToBottom();
return div.querySelector('.msg-content');
}
function renderMarkdown(text) {
if (!text) return '';
try { return marked.parse(text); } catch (e) { return escapeHtml(text).replace(/\n/g, '<br>'); }
}
function escapeHtml(text) { const d = document.createElement('div'); d.textContent = text; return d.innerHTML; }
function scrollToBottom() { const c = document.getElementById('chatMessages'); c.scrollTop = c.scrollHeight; }
// 初始化
(async function init() {
await loadSessions();
const saved = localStorage.getItem('llm_chat_current');
if (saved && parseInt(saved) > 0) {
currentSessionId = parseInt(saved);
// 验证该会话是否存在
const res = await fetch('llm-chat-api.php?action=sessions');
const data = await res.json();
const exists = (data.sessions || []).some(s => s.session_id == currentSessionId);
if (exists) {
await loadMessages(currentSessionId);
await loadSessions();
} else {
currentSessionId = 0;
localStorage.removeItem('llm_chat_current');
}
}
})();
</script>
<?php include("template/$OJ_TEMPLATE/footer.php"); ?>