feat: 网页端优化 - Markdown渲染、复制按钮、快捷语句右侧布局
This commit is contained in:
+242
-414
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>AI 对话系统 v2.0</title>
|
<title>AI 对话系统 v2.0</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.2.0/github-markdown-light.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; height: 100vh; display: flex; }
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; height: 100vh; display: flex; }
|
||||||
@@ -25,81 +26,88 @@
|
|||||||
.chat-header { padding: 16px 24px; border-bottom: 1px solid #e5e5e5; display: flex; align-items: center; justify-content: space-between; }
|
.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; }
|
.chat-header h1 { font-size: 18px; font-weight: 600; }
|
||||||
.header-controls { display: flex; align-items: center; gap: 16px; }
|
.header-controls { display: flex; align-items: center; gap: 16px; }
|
||||||
|
|
||||||
/* Agent选择器 */
|
|
||||||
.agent-selector { display: flex; align-items: center; gap: 8px; }
|
.agent-selector { display: flex; align-items: center; gap: 8px; }
|
||||||
.agent-selector label { font-size: 14px; color: #666; }
|
.agent-selector select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; cursor: pointer; min-width: 150px; }
|
||||||
.agent-selector select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; background: #fff; cursor: pointer; min-width: 150px; }
|
.ws-status { font-size: 12px; color: #666; background: #f0f0f0; padding: 4px 8px; border-radius: 4px; }
|
||||||
.agent-selector select:focus { border-color: #10a37f; outline: none; }
|
.ws-status.connected { color: #10a37f; }
|
||||||
|
|
||||||
.messages-container { flex: 1; overflow-y: auto; padding: 24px; }
|
.messages-container { flex: 1; overflow-y: auto; padding: 24px; }
|
||||||
.message { max-width: 800px; margin: 0 auto 24px; display: flex; gap: 16px; }
|
.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-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.user .message-avatar { background: #5436da; color: #fff; }
|
||||||
.message.assistant .message-avatar { background: #19c37d; color: #fff; }
|
.message.assistant .message-avatar { background: #19c37d; color: #fff; }
|
||||||
.message-content { flex: 1; line-height: 1.6; }
|
.message-body { flex: 1; }
|
||||||
.message-content pre { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 12px 0; }
|
.message-content { line-height: 1.6; position: relative; }
|
||||||
.message-content code { background: #f0f0f0; padding: 2px 6px; border-radius: 4px; font-family: monospace; }
|
|
||||||
.message-content pre code { background: transparent; padding: 0; }
|
/* Markdown样式 */
|
||||||
|
.markdown-body { font-size: 15px; }
|
||||||
|
.markdown-body pre { background: #f6f8fa; border-radius: 6px; padding: 16px; overflow-x: auto; }
|
||||||
|
.markdown-body code { background: #f6f8fa; padding: 2px 6px; border-radius: 4px; font-size: 85%; }
|
||||||
|
.markdown-body pre code { background: transparent; padding: 0; }
|
||||||
|
.markdown-body blockquote { border-left: 4px solid #dfe2e5; padding-left: 16px; color: #6a737d; }
|
||||||
|
|
||||||
|
/* 用户消息样式 */
|
||||||
|
.user-message-text { background: #f0f0f0; padding: 12px 16px; border-radius: 12px; font-size: 15px; }
|
||||||
|
|
||||||
|
/* 复制按钮 */
|
||||||
|
.copy-btn { position: absolute; top: 8px; right: 8px; padding: 4px 8px; background: rgba(0,0,0,0.05); border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-size: 12px; color: #666; opacity: 0; transition: opacity 0.2s; }
|
||||||
|
.message-content:hover .copy-btn { opacity: 1; }
|
||||||
|
.copy-btn:hover { background: rgba(0,0,0,0.1); }
|
||||||
|
.copy-btn.copied { color: #10a37f; border-color: #10a37f; }
|
||||||
|
|
||||||
/* 思考内容样式 */
|
/* 思考内容样式 */
|
||||||
.thinking-content { background: #f8f9fa; border-left: 3px solid #667eea; padding: 12px 16px; margin: 8px 0; font-size: 14px; color: #666; border-radius: 4px; }
|
.thinking-block { background: #f8f9fa; border-left: 3px solid #667eea; padding: 12px 16px; margin: 8px 0 16px 0; font-size: 14px; color: #666; border-radius: 4px; position: relative; }
|
||||||
.thinking-toggle { cursor: pointer; color: #667eea; font-size: 12px; margin-top: 4px; }
|
.thinking-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
|
||||||
|
.thinking-header span { font-weight: 500; color: #667eea; }
|
||||||
|
.thinking-toggle { font-size: 12px; color: #667eea; }
|
||||||
|
.thinking-content { margin-top: 12px; display: none; }
|
||||||
|
.thinking-content.expanded { display: block; }
|
||||||
|
|
||||||
.input-container { padding: 24px; border-top: 1px solid #e5e5e5; }
|
/* Agent信息 */
|
||||||
.input-wrapper { max-width: 800px; margin: 0 auto; display: flex; gap: 12px; align-items: flex-end; }
|
.agent-info { font-size: 12px; color: #999; margin-top: 8px; }
|
||||||
.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; }
|
/* 输入区域 */
|
||||||
|
.input-container { padding: 16px 24px; border-top: 1px solid #e5e5e5; }
|
||||||
|
.input-area { max-width: 800px; margin: 0 auto; display: flex; gap: 12px; align-items: flex-start; }
|
||||||
|
.input-box { flex: 1; display: flex; gap: 8px; align-items: flex-end; }
|
||||||
|
.input-box textarea { flex: 1; padding: 12px 16px; border: 1px solid #ddd; border-radius: 12px; font-size: 16px; resize: none; outline: none; max-height: 200px; line-height: 1.5; }
|
||||||
|
.input-box 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 { 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:hover { background: #0d8c6d; }
|
||||||
.send-btn:disabled { background: #ccc; cursor: not-allowed; }
|
.send-btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||||
|
|
||||||
/* 快捷语句 */
|
/* 快捷语句 - 右侧 */
|
||||||
.quick-phrases { max-width: 800px; margin: 0 auto; padding-top: 12px; }
|
.quick-phrases-panel { width: 200px; padding: 8px; background: #f8f9fa; border-radius: 12px; max-height: 300px; overflow-y: auto; }
|
||||||
.quick-phrases-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
.quick-phrases-title { font-size: 12px; color: #999; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; }
|
||||||
.quick-phrases-header span { font-size: 12px; color: #999; }
|
.quick-phrases-title button { background: none; border: 1px solid #ddd; padding: 2px 6px; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
||||||
.quick-phrases-header button { font-size: 12px; background: none; border: 1px solid #ddd; padding: 4px 8px; border-radius: 4px; cursor: pointer; color: #666; }
|
.quick-phrases-list { display: flex; flex-direction: column; gap: 6px; }
|
||||||
.quick-phrases-header button:hover { background: #f5f5f5; }
|
.quick-phrase-item { padding: 8px 12px; background: #fff; border: 1px solid #e0e0e0; border-radius: 6px; cursor: pointer; font-size: 13px; color: #333; transition: all 0.2s; position: relative; }
|
||||||
.quick-phrases-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
.quick-phrase-item:hover { background: #e8f5e9; border-color: #10a37f; }
|
||||||
.quick-phrase-item { display: flex; align-items: center; gap: 4px; padding: 6px 12px; background: #f0f0f0; border-radius: 16px; cursor: pointer; font-size: 13px; color: #333; transition: all 0.2s; border: 1px solid transparent; }
|
.quick-phrase-item .phrase-delete { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); opacity: 0; cursor: pointer; color: #999; }
|
||||||
.quick-phrase-item:hover { background: #e8e8e8; border-color: #10a37f; }
|
.quick-phrase-item:hover .phrase-delete { opacity: 1; }
|
||||||
.quick-phrase-item .delete-btn { opacity: 0; font-size: 12px; color: #999; margin-left: 4px; }
|
|
||||||
.quick-phrase-item:hover .delete-btn { opacity: 1; }
|
|
||||||
|
|
||||||
/* 快捷语句管理弹窗 */
|
/* 弹窗 */
|
||||||
.phrase-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
|
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
|
||||||
.phrase-modal.show { display: flex; }
|
.modal-overlay.show { display: flex; }
|
||||||
.phrase-modal-content { background: #fff; padding: 24px; border-radius: 12px; max-width: 400px; width: 90%; }
|
.modal-box { background: #fff; padding: 24px; border-radius: 12px; max-width: 400px; width: 90%; }
|
||||||
.phrase-modal-content h3 { margin-bottom: 16px; }
|
.modal-box h3 { margin-bottom: 16px; }
|
||||||
.phrase-modal-content input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 16px; }
|
.modal-box input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 16px; }
|
||||||
.phrase-modal-buttons { display: flex; gap: 12px; justify-content: flex-end; }
|
.modal-buttons { display: flex; gap: 12px; justify-content: flex-end; }
|
||||||
.phrase-modal-buttons button { padding: 8px 16px; border-radius: 8px; cursor: pointer; }
|
.modal-buttons button { padding: 8px 16px; border-radius: 8px; cursor: pointer; }
|
||||||
.phrase-modal-buttons .cancel { background: #f5f5f5; border: 1px solid #ddd; }
|
|
||||||
.phrase-modal-buttons .save { background: #10a37f; color: #fff; border: none; }
|
|
||||||
|
|
||||||
.welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #666; }
|
.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 h2 { font-size: 28px; margin-bottom: 16px; color: #333; }
|
||||||
.welcome p { font-size: 16px; }
|
|
||||||
|
|
||||||
.loading::after { content: ''; animation: dots 1.5s infinite; }
|
::-webkit-scrollbar { width: 6px; }
|
||||||
@keyframes dots { 0%, 20% { content: '.'; } 40% { content: '..'; } 60%, 100% { content: '...'; } }
|
::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; }
|
||||||
|
|
||||||
::-webkit-scrollbar { width: 8px; }
|
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
|
||||||
::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
|
|
||||||
::-webkit-scrollbar-thumb:hover { background: #a1a1a1; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<button class="new-chat-btn" onclick="createNewConversation()">
|
<button class="new-chat-btn" onclick="createNewConversation()"><i class="ri-add-line"></i> 新对话</button>
|
||||||
<i class="ri-add-line"></i> 新对话
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="conversation-list" id="conversationList">
|
|
||||||
<div class="empty-state" style="text-align:center;color:#999;padding:40px;">暂无对话</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="conversation-list" id="conversationList"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
@@ -107,373 +115,168 @@
|
|||||||
<h1>AI 对话 v2.0</h1>
|
<h1>AI 对话 v2.0</h1>
|
||||||
<div class="header-controls">
|
<div class="header-controls">
|
||||||
<div class="agent-selector">
|
<div class="agent-selector">
|
||||||
<label><i class="ri-robot-line"></i> Agent:</label>
|
<select id="agentSelect" onchange="switchAgent()"><option value="">加载中...</option></select>
|
||||||
<select id="agentSelect" onchange="switchAgent()">
|
|
||||||
<option value="">加载中...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div style="font-size:12px;color:#666;background:#f0f0f0;padding:4px 8px;border-radius:4px;">
|
|
||||||
<i class="ri-user-line"></i> 主用户 <span id="wsStatus" style="color:#999;">连接中...</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ws-status" id="wsStatus">连接中...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="messages-container" id="messagesContainer">
|
<div class="messages-container" id="messagesContainer">
|
||||||
<div class="welcome">
|
<div class="welcome"><h2>👋 开始对话</h2><p>选择Agent,开始聊天</p></div>
|
||||||
<h2>👋 欢迎使用 AI 对话系统</h2>
|
|
||||||
<p>选择Agent,开始对话吧</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<div class="input-wrapper">
|
<div class="input-area">
|
||||||
<textarea id="messageInput" placeholder="输入消息... (Shift+Enter换行)" rows="1"></textarea>
|
<div class="input-box">
|
||||||
<button class="send-btn" id="sendBtn" onclick="sendMessage()">
|
<textarea id="messageInput" placeholder="输入消息..." rows="1"></textarea>
|
||||||
<i class="ri-send-plane-fill"></i>
|
<button class="send-btn" id="sendBtn" onclick="sendMessage()"><i class="ri-send-plane-fill"></i></button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<!-- 快捷语句右侧 -->
|
||||||
|
<div class="quick-phrases-panel">
|
||||||
<!-- 快捷语句 -->
|
<div class="quick-phrases-title">
|
||||||
<div class="quick-phrases">
|
<span><i class="ri-flashlight-line"></i> 快捷语句</span>
|
||||||
<div class="quick-phrases-header">
|
<button onclick="showAddPhraseModal()"><i class="ri-add-line"></i></button>
|
||||||
<span><i class="ri-flashlight-line"></i> 快捷语句</span>
|
</div>
|
||||||
<button onclick="showAddPhraseModal()"><i class="ri-add-line"></i> 添加</button>
|
<div class="quick-phrases-list" id="quickPhrasesList"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-phrases-list" id="quickPhrasesList"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加快捷语句弹窗 -->
|
<!-- 添加快捷语句弹窗 -->
|
||||||
<div class="phrase-modal" id="phraseModal">
|
<div class="modal-overlay" id="phraseModal">
|
||||||
<div class="phrase-modal-content">
|
<div class="modal-box">
|
||||||
<h3><i class="ri-add-line"></i> 添加快捷语句</h3>
|
<h3><i class="ri-add-line"></i> 添加快捷语句</h3>
|
||||||
<input type="text" id="newPhraseInput" placeholder="输入快捷语句内容..." maxlength="100">
|
<input type="text" id="newPhraseInput" placeholder="输入内容..." maxlength="100">
|
||||||
<div class="phrase-modal-buttons">
|
<div class="modal-buttons">
|
||||||
<button class="cancel" onclick="hidePhraseModal()">取消</button>
|
<button style="background:#f5f5f5;border:1px solid #ddd;" onclick="hidePhraseModal()">取消</button>
|
||||||
<button class="save" onclick="addPhrase()">添加</button>
|
<button style="background:#10a37f;color:#fff;border:none;" onclick="addPhrase()">添加</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Markdown渲染库 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 全局变量
|
marked.setOptions({ breaks: true, gfm: true });
|
||||||
|
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let userId = 'main_user';
|
let userId = 'main_user';
|
||||||
let currentConversationId = null;
|
let currentConversationId = null;
|
||||||
let currentAgentId = null;
|
let currentAgentId = null;
|
||||||
let conversations = [];
|
|
||||||
let agents = [];
|
let agents = [];
|
||||||
let quickPhrases = [];
|
let quickPhrases = [];
|
||||||
|
|
||||||
// 默认快捷语句
|
|
||||||
const DEFAULT_phrases = [
|
|
||||||
'请帮我总结一下',
|
|
||||||
'帮我分析这个问题',
|
|
||||||
'用简单的话解释一下',
|
|
||||||
'给我举个例子',
|
|
||||||
'有什么建议吗?'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadAgents();
|
loadAgents();
|
||||||
loadQuickPhrases();
|
loadQuickPhrases();
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
loadConversations();
|
loadConversations();
|
||||||
setupTextarea();
|
setupTextarea();
|
||||||
setInterval(checkLatestConversation, 3000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载Agent列表
|
// 加载Agent
|
||||||
async function loadAgents() {
|
async function loadAgents() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/v2/agents');
|
const res = await fetch('/api/v2/agents');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
agents = data.agents || [];
|
agents = data.agents || [];
|
||||||
|
|
||||||
// 先设置当前Agent,再渲染
|
|
||||||
const defaultAgent = agents.find(a => a.is_default) || agents[0];
|
const defaultAgent = agents.find(a => a.is_default) || agents[0];
|
||||||
if (defaultAgent) currentAgentId = defaultAgent.id;
|
if (defaultAgent) currentAgentId = defaultAgent.id;
|
||||||
|
|
||||||
renderAgentSelect();
|
renderAgentSelect();
|
||||||
} catch (e) {
|
} catch (e) { console.error('加载Agent失败:', e); }
|
||||||
console.error('加载Agent失败:', e);
|
|
||||||
document.getElementById('agentSelect').innerHTML = '<option value="">加载失败</option>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染Agent下拉框
|
function renderAgentSelect() {
|
||||||
function renderAgentSelect(selectedId = null) {
|
|
||||||
const select = document.getElementById('agentSelect');
|
const select = document.getElementById('agentSelect');
|
||||||
const selected = selectedId || currentAgentId;
|
|
||||||
select.innerHTML = agents.filter(a => a.is_active).map(a =>
|
select.innerHTML = agents.filter(a => a.is_active).map(a =>
|
||||||
`<option value="${a.id}" ${a.id === selected ? 'selected' : ''}>${a.display_name || a.name}</option>`
|
`<option value="${a.id}" ${a.id === currentAgentId ? 'selected' : ''}>${a.display_name || a.name}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新Agent选择框显示
|
|
||||||
function updateAgentSelect(agentId) {
|
function updateAgentSelect(agentId) {
|
||||||
const select = document.getElementById('agentSelect');
|
|
||||||
select.value = agentId;
|
|
||||||
currentAgentId = agentId;
|
currentAgentId = agentId;
|
||||||
|
document.getElementById('agentSelect').value = agentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换Agent - 自动创建新对话
|
|
||||||
async function switchAgent() {
|
async function switchAgent() {
|
||||||
const newAgentId = document.getElementById('agentSelect').value;
|
const newAgentId = parseInt(document.getElementById('agentSelect').value);
|
||||||
if (newAgentId && parseInt(newAgentId) !== currentAgentId) {
|
if (newAgentId && newAgentId !== currentAgentId) {
|
||||||
const oldAgentId = currentAgentId;
|
currentAgentId = newAgentId;
|
||||||
currentAgentId = parseInt(newAgentId);
|
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ action: 'switch_agent', agent_id: currentAgentId }));
|
||||||
console.log('切换Agent:', currentAgentId);
|
|
||||||
|
|
||||||
// 发送切换消息到WebSocket
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
action: 'switch_agent',
|
|
||||||
agent_id: currentAgentId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动创建新对话
|
|
||||||
await createNewConversation();
|
await createNewConversation();
|
||||||
|
showAgentSwitchNotice();
|
||||||
// 显示切换提示
|
|
||||||
const agent = agents.find(a => a.id === currentAgentId);
|
|
||||||
if (agent) {
|
|
||||||
showAgentSwitchNotice(agent.display_name || agent.name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示Agent切换提示
|
function showAgentSwitchNotice() {
|
||||||
function showAgentSwitchNotice(agentName) {
|
const agent = agents.find(a => a.id === currentAgentId);
|
||||||
const container = document.getElementById('messagesContainer');
|
document.getElementById('messagesContainer').innerHTML = `
|
||||||
// 清空现有内容,显示切换提示
|
<div class="welcome"><h2>🔄 已切换 Agent</h2><p style="color:#667eea;font-size:18px;"><strong>${agent?.display_name || '助手'}</strong></p></div>
|
||||||
container.innerHTML = `
|
|
||||||
<div class="welcome">
|
|
||||||
<h2>🔄 已切换 Agent</h2>
|
|
||||||
<p style="color:#667eea;font-size:18px;"><strong>${agentName}</strong></p>
|
|
||||||
<p style="color:#999;font-size:14px;">已为您创建新对话,可直接开始聊天</p>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载快捷语句
|
// WebSocket
|
||||||
function loadQuickPhrases() {
|
|
||||||
const saved = localStorage.getItem('quickPhrases');
|
|
||||||
quickPhrases = saved ? JSON.parse(saved) : DEFAULT_phrases;
|
|
||||||
renderQuickPhrases();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染快捷语句
|
|
||||||
function renderQuickPhrases() {
|
|
||||||
const container = document.getElementById('quickPhrasesList');
|
|
||||||
if (quickPhrases.length === 0) {
|
|
||||||
container.innerHTML = '<span style="color:#999;font-size:12px;">点击"添加"创建快捷语句</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
container.innerHTML = quickPhrases.map((p, i) => `
|
|
||||||
<div class="quick-phrase-item" onclick="usePhrase(${i})">
|
|
||||||
<span>${p}</span>
|
|
||||||
<span class="delete-btn" onclick="deletePhrase(${i}, event)"><i class="ri-close-line"></i></span>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用快捷语句
|
|
||||||
function usePhrase(index) {
|
|
||||||
const input = document.getElementById('messageInput');
|
|
||||||
input.value = quickPhrases[index];
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除快捷语句
|
|
||||||
function deletePhrase(index, event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
quickPhrases.splice(index, 1);
|
|
||||||
localStorage.setItem('quickPhrases', JSON.stringify(quickPhrases));
|
|
||||||
renderQuickPhrases();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示添加弹窗
|
|
||||||
function showAddPhraseModal() {
|
|
||||||
document.getElementById('phraseModal').classList.add('show');
|
|
||||||
document.getElementById('newPhraseInput').value = '';
|
|
||||||
document.getElementById('newPhraseInput').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏弹窗
|
|
||||||
function hidePhraseModal() {
|
|
||||||
document.getElementById('phraseModal').classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加快捷语句
|
|
||||||
function addPhrase() {
|
|
||||||
const input = document.getElementById('newPhraseInput');
|
|
||||||
const phrase = input.value.trim();
|
|
||||||
if (phrase) {
|
|
||||||
quickPhrases.push(phrase);
|
|
||||||
localStorage.setItem('quickPhrases', JSON.stringify(quickPhrases));
|
|
||||||
renderQuickPhrases();
|
|
||||||
hidePhraseModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查最新会话
|
|
||||||
async function checkLatestConversation() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/conversations/latest');
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.conversation && data.conversation.id !== currentConversationId) {
|
|
||||||
selectConversation(data.conversation.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket连接
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const wsUrl = `${protocol}//${window.location.host}/ws/${userId}`;
|
ws = new WebSocket(`${protocol}//${window.location.host}/ws/${userId}`);
|
||||||
console.log('WebSocket连接:', wsUrl);
|
ws.onopen = () => { document.getElementById('wsStatus').textContent = '已连接'; document.getElementById('wsStatus').classList.add('connected'); };
|
||||||
ws = new WebSocket(wsUrl);
|
ws.onmessage = (e) => handleWebSocketMessage(JSON.parse(e.data));
|
||||||
|
ws.onclose = () => { document.getElementById('wsStatus').textContent = '断开'; document.getElementById('wsStatus').classList.remove('connected'); setTimeout(connectWebSocket, 3000); };
|
||||||
ws.onopen = () => {
|
ws.onerror = () => document.getElementById('wsStatus').textContent = '错误';
|
||||||
console.log('WebSocket已连接');
|
|
||||||
const wsStatus = document.getElementById('wsStatus');
|
|
||||||
if (wsStatus) wsStatus.textContent = '已连接';
|
|
||||||
};
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
console.log('WebSocket收到消息:', event.data);
|
|
||||||
handleWebSocketMessage(JSON.parse(event.data));
|
|
||||||
};
|
|
||||||
ws.onclose = (e) => {
|
|
||||||
console.log('WebSocket断开:', e.code, e.reason);
|
|
||||||
const wsStatus = document.getElementById('wsStatus');
|
|
||||||
if (wsStatus) wsStatus.textContent = '断开';
|
|
||||||
setTimeout(connectWebSocket, 3000);
|
|
||||||
};
|
|
||||||
ws.onerror = (e) => {
|
|
||||||
console.error('WebSocket错误:', e);
|
|
||||||
const wsStatus = document.getElementById('wsStatus');
|
|
||||||
if (wsStatus) wsStatus.textContent = '错误';
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理WebSocket消息
|
|
||||||
function handleWebSocketMessage(data) {
|
function handleWebSocketMessage(data) {
|
||||||
console.log('收到消息:', data.type);
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'history':
|
case 'history': displayHistory(data.messages); if (data.agent_id) updateAgentSelect(data.agent_id); break;
|
||||||
displayHistory(data.messages);
|
case 'conversation_created': currentConversationId = data.conversation_id; loadConversations(); break;
|
||||||
if (data.agent_id) {
|
case 'new_conversation': currentConversationId = data.conversation_id; loadConversations(); clearMessages(); break;
|
||||||
currentAgentId = data.agent_id;
|
case 'agent_switched': currentAgentId = data.agent_id; break;
|
||||||
updateAgentSelect(data.agent_id);
|
case 'stream_end': document.getElementById('sendBtn').disabled = false; break;
|
||||||
}
|
case 'user_message': appendMessage('user', data.message.content); break;
|
||||||
break;
|
case 'assistant_message': appendMessage('assistant', data.message.content, data.message.thinking_content, data.message.agent_name); document.getElementById('sendBtn').disabled = false; break;
|
||||||
case 'conversation_created':
|
case 'error': showError(data.message); document.getElementById('sendBtn').disabled = false; break;
|
||||||
currentConversationId = data.conversation_id;
|
|
||||||
loadConversations();
|
|
||||||
break;
|
|
||||||
case 'new_conversation':
|
|
||||||
currentConversationId = data.conversation_id;
|
|
||||||
loadConversations();
|
|
||||||
clearMessages();
|
|
||||||
break;
|
|
||||||
case 'agent_switched':
|
|
||||||
currentAgentId = data.agent_id;
|
|
||||||
console.log('Agent已切换:', data.agent_name);
|
|
||||||
break;
|
|
||||||
case 'ping':
|
|
||||||
// 心跳,忽略
|
|
||||||
break;
|
|
||||||
case 'stream_end':
|
|
||||||
document.getElementById('sendBtn').disabled = false;
|
|
||||||
break;
|
|
||||||
case 'user_message':
|
|
||||||
appendMessage('user', data.message.content, data.message.source);
|
|
||||||
if (data.message.source === 'matrix' && data.conversation_id) {
|
|
||||||
currentConversationId = data.conversation_id;
|
|
||||||
renderConversations();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'assistant_message':
|
|
||||||
// 直接显示完整回复
|
|
||||||
appendMessage('assistant', data.message.content, data.message.source, data.message.thinking_content, data.message.agent_name);
|
|
||||||
document.getElementById('sendBtn').disabled = false;
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
showError(data.message);
|
|
||||||
document.getElementById('sendBtn').disabled = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示错误
|
// 消息渲染
|
||||||
function showError(msg) {
|
function appendMessage(role, content, thinking = null, agentName = null) {
|
||||||
const container = document.getElementById('messagesContainer');
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'message assistant';
|
|
||||||
div.innerHTML = `
|
|
||||||
<div class="message-avatar">❌</div>
|
|
||||||
<div class="message-content" style="color:#dc3545;">${msg}</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空消息
|
|
||||||
function clearMessages() {
|
|
||||||
document.getElementById('messagesContainer').innerHTML = `
|
|
||||||
<div class="welcome">
|
|
||||||
<h2>👋 开始新对话</h2>
|
|
||||||
<p>当前Agent: ${getAgentName()}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前Agent名称
|
|
||||||
function getAgentName() {
|
|
||||||
const agent = agents.find(a => a.id === currentAgentId);
|
|
||||||
return agent ? (agent.display_name || agent.name) : '默认助手';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示历史消息
|
|
||||||
function displayHistory(messages) {
|
|
||||||
const container = document.getElementById('messagesContainer');
|
|
||||||
container.innerHTML = '';
|
|
||||||
messages.forEach(msg => {
|
|
||||||
appendMessage(msg.role, msg.content, msg.source, msg.thinking_content);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加消息
|
|
||||||
function appendMessage(role, content, source = 'web', thinking = null, agentName = null) {
|
|
||||||
const container = document.getElementById('messagesContainer');
|
const container = document.getElementById('messagesContainer');
|
||||||
container.querySelector('.welcome')?.remove();
|
container.querySelector('.welcome')?.remove();
|
||||||
document.getElementById('thinkingIndicator')?.remove();
|
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = `message ${role}`;
|
div.className = `message ${role}`;
|
||||||
|
|
||||||
const avatar = role === 'user' ? '👤' : '🤖';
|
const avatar = role === 'user' ? '👤' : '🤖';
|
||||||
let html = `<div class="message-avatar">${avatar}</div><div class="message-content">`;
|
let html = `<div class="message-avatar">${avatar}</div><div class="message-body">`;
|
||||||
|
|
||||||
// 思考内容
|
// 思考内容
|
||||||
if (thinking) {
|
if (thinking) {
|
||||||
html += `<div class="thinking-content" id="thinking-${Date.now()}">
|
html += `<div class="thinking-block">
|
||||||
<strong>💭 思考过程:</strong><br>${formatContent(thinking)}
|
<div class="thinking-header" onclick="toggleThinking(this)">
|
||||||
<div class="thinking-toggle" onclick="toggleThinking(this)">收起</div>
|
<span><i class="ri-lightbulb-line"></i> 思考过程</span>
|
||||||
|
<span class="thinking-toggle">点击展开</span>
|
||||||
|
</div>
|
||||||
|
<div class="thinking-content">${escapeHtml(thinking)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主要内容
|
// 消息内容
|
||||||
html += formatContent(content);
|
html += `<div class="message-content">`;
|
||||||
|
if (role === 'assistant') {
|
||||||
|
// AI消息用Markdown渲染
|
||||||
|
html += `<div class="markdown-body">${marked.parse(content)}</div>`;
|
||||||
|
} else {
|
||||||
|
// 用户消息普通显示
|
||||||
|
html += `<div class="user-message-text">${escapeHtml(content)}</div>`;
|
||||||
|
}
|
||||||
|
// 复制按钮
|
||||||
|
html += `<button class="copy-btn" onclick="copyMessage(this, '${escapeHtml(content).replace(/'/g, "\\'")}')"><i class="ri-copy-line"></i> 复制</button>`;
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
// Agent信息
|
// Agent信息
|
||||||
if (role === 'assistant' && agentName) {
|
if (role === 'assistant' && agentName) {
|
||||||
html += `<div style="font-size:12px;color:#999;margin-top:8px;"><i class="ri-robot-line"></i> ${agentName}</div>`;
|
html += `<div class="agent-info"><i class="ri-robot-line"></i> ${agentName}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -482,89 +285,90 @@
|
|||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换思考内容显示
|
function toggleThinking(header) {
|
||||||
function toggleThinking(btn) {
|
const block = header.parentElement;
|
||||||
const content = btn.parentElement;
|
const content = block.querySelector('.thinking-content');
|
||||||
const isCollapsed = content.style.maxHeight;
|
const toggle = header.querySelector('.thinking-toggle');
|
||||||
if (isCollapsed) {
|
if (content.classList.contains('expanded')) {
|
||||||
content.style.maxHeight = '';
|
content.classList.remove('expanded');
|
||||||
content.style.overflow = '';
|
toggle.textContent = '点击展开';
|
||||||
btn.textContent = '收起';
|
|
||||||
} else {
|
} else {
|
||||||
content.style.maxHeight = '60px';
|
content.classList.add('expanded');
|
||||||
content.style.overflow = 'hidden';
|
toggle.textContent = '点击折叠';
|
||||||
btn.textContent = '展开';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化内容
|
function copyMessage(btn, text) {
|
||||||
function formatContent(content) {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
return content
|
btn.innerHTML = '<i class="ri-check-line"></i> 已复制';
|
||||||
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
btn.classList.add('copied');
|
||||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
setTimeout(() => { btn.innerHTML = '<i class="ri-copy-line"></i> 复制'; btn.classList.remove('copied'); }, 2000);
|
||||||
.replace(/\n/g, '<br>');
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载会话列表
|
function escapeHtml(text) {
|
||||||
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayHistory(messages) {
|
||||||
|
const container = document.getElementById('messagesContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
messages.forEach(m => appendMessage(m.role, m.content, m.thinking_content));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMessages() {
|
||||||
|
document.getElementById('messagesContainer').innerHTML = '<div class="welcome"><h2>👋 开始对话</h2></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
const container = document.getElementById('messagesContainer');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'message assistant';
|
||||||
|
div.innerHTML = `<div class="message-avatar">❌</div><div class="message-body"><div class="message-content" style="color:#dc3545;">${msg}</div></div>`;
|
||||||
|
container.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 会话管理
|
||||||
async function loadConversations() {
|
async function loadConversations() {
|
||||||
try {
|
const res = await fetch('/api/conversations');
|
||||||
const res = await fetch('/api/conversations');
|
const data = await res.json();
|
||||||
const data = await res.json();
|
const conversations = data.conversations || [];
|
||||||
conversations = data.conversations || [];
|
renderConversations(conversations);
|
||||||
renderConversations();
|
if (!currentConversationId && conversations.length > 0) selectConversation(conversations[0].id);
|
||||||
if (!currentConversationId && conversations.length > 0) {
|
|
||||||
selectConversation(conversations[0].id);
|
|
||||||
}
|
|
||||||
} catch (e) { console.error('加载会话失败:', e); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染会话列表
|
function renderConversations(list) {
|
||||||
function renderConversations() {
|
|
||||||
const container = document.getElementById('conversationList');
|
const container = document.getElementById('conversationList');
|
||||||
if (conversations.length === 0) {
|
if (list.length === 0) { container.innerHTML = '<div style="text-align:center;color:#999;padding:40px;">暂无对话</div>'; return; }
|
||||||
container.innerHTML = '<div style="text-align:center;color:#999;padding:40px;">暂无对话</div>';
|
container.innerHTML = list.map(c => `
|
||||||
return;
|
|
||||||
}
|
|
||||||
container.innerHTML = conversations.map(c => `
|
|
||||||
<div class="conversation-item ${c.id === currentConversationId ? 'active' : ''}" onclick="selectConversation('${c.id}')">
|
<div class="conversation-item ${c.id === currentConversationId ? 'active' : ''}" onclick="selectConversation('${c.id}')">
|
||||||
<span class="title">${c.title}</span>
|
<span class="title">${c.title || '新对话'}</span>
|
||||||
<button class="delete-btn" onclick="deleteConversation('${c.id}', event)"><i class="ri-delete-bin-line"></i></button>
|
<button class="delete-btn" onclick="deleteConversation('${c.id}',event)"><i class="ri-delete-bin-line"></i></button>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择会话
|
|
||||||
function selectConversation(id) {
|
function selectConversation(id) {
|
||||||
currentConversationId = id;
|
currentConversationId = id;
|
||||||
renderConversations();
|
renderConversations([]);
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
loadConversations();
|
||||||
ws.send(JSON.stringify({ action: 'select_conversation', conversation_id: id }));
|
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ action: 'select_conversation', conversation_id: id }));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新会话
|
|
||||||
async function createNewConversation() {
|
async function createNewConversation() {
|
||||||
try {
|
const res = await fetch('/api/conversations', { method: 'POST' });
|
||||||
const res = await fetch('/api/conversations', { method: 'POST' });
|
const data = await res.json();
|
||||||
const data = await res.json();
|
currentConversationId = data.id;
|
||||||
currentConversationId = data.id;
|
clearMessages();
|
||||||
clearMessages();
|
loadConversations();
|
||||||
loadConversations();
|
|
||||||
} catch (e) { console.error('创建会话失败:', e); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除会话
|
async function deleteConversation(id, e) {
|
||||||
async function deleteConversation(id, event) {
|
e.stopPropagation();
|
||||||
event.stopPropagation();
|
|
||||||
if (!confirm('确定删除?')) return;
|
if (!confirm('确定删除?')) return;
|
||||||
try {
|
await fetch(`/api/conversations/${id}`, { method: 'DELETE' });
|
||||||
await fetch(`/api/conversations/${id}`, { method: 'DELETE' });
|
if (id === currentConversationId) { currentConversationId = null; clearMessages(); }
|
||||||
if (id === currentConversationId) {
|
loadConversations();
|
||||||
currentConversationId = null;
|
|
||||||
clearMessages();
|
|
||||||
}
|
|
||||||
loadConversations();
|
|
||||||
} catch (e) { console.error('删除失败:', e); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
@@ -572,41 +376,65 @@
|
|||||||
const input = document.getElementById('messageInput');
|
const input = document.getElementById('messageInput');
|
||||||
const msg = input.value.trim();
|
const msg = input.value.trim();
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
document.getElementById('sendBtn').disabled = true;
|
document.getElementById('sendBtn').disabled = true;
|
||||||
input.value = '';
|
input.value = '';
|
||||||
input.style.height = 'auto';
|
input.style.height = 'auto';
|
||||||
|
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({ action: 'chat', message: msg, conversation_id: currentConversationId, agent_id: currentAgentId }));
|
||||||
action: 'chat',
|
|
||||||
message: msg,
|
|
||||||
conversation_id: currentConversationId,
|
|
||||||
agent_id: currentAgentId
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置文本框
|
|
||||||
function setupTextarea() {
|
function setupTextarea() {
|
||||||
const textarea = document.getElementById('messageInput');
|
const textarea = document.getElementById('messageInput');
|
||||||
textarea.addEventListener('keydown', (e) => {
|
textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
textarea.addEventListener('input', () => { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'; });
|
||||||
e.preventDefault();
|
|
||||||
sendMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
textarea.addEventListener('input', () => {
|
|
||||||
textarea.style.height = 'auto';
|
|
||||||
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 弹窗键盘事件
|
// 快捷语句
|
||||||
document.getElementById('newPhraseInput').addEventListener('keydown', (e) => {
|
function loadQuickPhrases() {
|
||||||
if (e.key === 'Enter') addPhrase();
|
const saved = localStorage.getItem('quickPhrases');
|
||||||
if (e.key === 'Escape') hidePhraseModal();
|
quickPhrases = saved ? JSON.parse(saved) : ['请帮我总结一下', '帮我分析这个问题', '用简单的话解释一下', '给我举个例子', '有什么建议吗?'];
|
||||||
});
|
renderQuickPhrases();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQuickPhrases() {
|
||||||
|
const container = document.getElementById('quickPhrasesList');
|
||||||
|
container.innerHTML = quickPhrases.map((p, i) => `
|
||||||
|
<div class="quick-phrase-item" onclick="usePhrase('${escapeHtml(p)}')">${p}<span class="phrase-delete" onclick="deletePhrase(${i},event)"><i class="ri-close-line"></i></span></div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePhrase(text) {
|
||||||
|
document.getElementById('messageInput').value = text;
|
||||||
|
document.getElementById('messageInput').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePhrase(index, e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
quickPhrases.splice(index, 1);
|
||||||
|
localStorage.setItem('quickPhrases', JSON.stringify(quickPhrases));
|
||||||
|
renderQuickPhrases();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddPhraseModal() {
|
||||||
|
document.getElementById('phraseModal').classList.add('show');
|
||||||
|
document.getElementById('newPhraseInput').value = '';
|
||||||
|
document.getElementById('newPhraseInput').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePhraseModal() { document.getElementById('phraseModal').classList.remove('show'); }
|
||||||
|
|
||||||
|
function addPhrase() {
|
||||||
|
const phrase = document.getElementById('newPhraseInput').value.trim();
|
||||||
|
if (phrase) {
|
||||||
|
quickPhrases.push(phrase);
|
||||||
|
localStorage.setItem('quickPhrases', JSON.stringify(quickPhrases));
|
||||||
|
renderQuickPhrases();
|
||||||
|
hidePhraseModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('newPhraseInput').addEventListener('keydown', e => { if (e.key === 'Enter') addPhrase(); if (e.key === 'Escape') hidePhraseModal(); });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user