516 lines
16 KiB
PHP
516 lines
16 KiB
PHP
<?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"); ?>
|