qwen_agent/public/index.html
2025-11-08 20:23:04 +08:00

1138 lines
41 KiB
HTML
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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI聊天助手</title>
<!-- 引入 marked.js 用于 Markdown 解析 - 使用国内CDN -->
<script src="https://cdn.staticfile.net/marked/4.3.0/marked.min.js"></script>
<!-- 引入 highlight.js 用于代码高亮 - 使用国内CDN -->
<link rel="stylesheet" href="https://cdn.staticfile.net/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdn.staticfile.net/highlight.js/11.9.0/highlight.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
height: 100vh;
display: flex;
flex-direction: column;
}
.chat-container {
max-width: 900px;
margin: 0 auto;
height: 100%;
display: flex;
flex-direction: column;
background-color: white;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.chat-header {
background-color: #4a6cf7;
color: white;
padding: 15px 20px;
display: flex;
align-items: center;
border-bottom: 1px solid #e0e0e0;
}
.chat-title {
font-size: 18px;
font-weight: bold;
}
.chat-status {
margin-left: 10px;
font-size: 14px;
opacity: 0.8;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #4ade80;
margin-right: 5px;
}
.status-indicator.disconnected {
background-color: #f87171;
}
.status-indicator.connecting {
background-color: #fbbf24;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.message {
display: flex;
gap: 10px;
max-width: 80%;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message.assistant {
align-self: flex-start;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.message.user .message-avatar {
background-color: #4a6cf7;
color: white;
}
.message.assistant .message-avatar {
background-color: #f1f5f9;
color: #4a5568;
}
.message-content {
padding: 12px 16px;
border-radius: 18px;
max-width: 100%;
word-wrap: break-word;
}
.message.user .message-content {
background-color: #4a6cf7;
color: white;
border-bottom-right-radius: 6px;
}
.message.assistant .message-content {
background-color: #f1f5f9;
color: #1a202c;
border-bottom-left-radius: 6px;
}
/* Markdown 样式 */
.message-content {
line-height: 1.6;
}
.message-content h1,
.message-content h2,
.message-content h3,
.message-content h4,
.message-content h5,
.message-content h6 {
margin: 16px 0 8px 0;
font-weight: bold;
color: #1a202c;
}
.message-content h1 { font-size: 1.5em; }
.message-content h2 { font-size: 1.4em; }
.message-content h3 { font-size: 1.3em; }
.message-content h4 { font-size: 1.2em; }
.message-content h5 { font-size: 1.1em; }
.message-content h6 { font-size: 1em; }
.message-content p {
margin: 8px 0;
}
.message-content ul,
.message-content ol {
margin: 8px 0;
padding-left: 20px;
}
.message-content li {
margin: 4px 0;
}
.message-content blockquote {
margin: 8px 0;
padding: 8px 12px;
border-left: 4px solid #4a6cf7;
background-color: #f8fafc;
font-style: italic;
}
.message-content code {
background-color: #f1f5f9;
padding: 2px 4px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.message-content pre {
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 12px;
overflow-x: auto;
margin: 8px 0;
}
.message-content pre code {
background-color: transparent;
padding: 0;
border-radius: 0;
font-size: 0.9em;
}
.message-content table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
}
.message-content th,
.message-content td {
border: 1px solid #e2e8f0;
padding: 8px 12px;
text-align: left;
}
.message-content th {
background-color: #f8fafc;
font-weight: bold;
}
.message-content a {
color: #4a6cf7;
text-decoration: none;
}
.message-content a:hover {
text-decoration: underline;
}
.message-content strong {
font-weight: bold;
}
.message-content em {
font-style: italic;
}
.message-content hr {
border: none;
border-top: 1px solid #e2e8f0;
margin: 16px 0;
}
/* 代码高亮样式 */
.hljs {
background-color: #f8fafc !important;
color: #1a202c;
}
/* 用户消息的Markdown样式调整 */
.message.user .message-content h1,
.message.user .message-content h2,
.message.user .message-content h3,
.message.user .message-content h4,
.message.user .message-content h5,
.message.user .message-content h6 {
color: white;
}
.message.user .message-content blockquote {
background-color: rgba(255, 255, 255, 0.1);
border-left-color: rgba(255, 255, 255, 0.5);
}
.message.user .message-content code {
background-color: rgba(255, 255, 255, 0.1);
}
.message.user .message-content pre {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
}
.message.user .message-content th {
background-color: rgba(255, 255, 255, 0.1);
}
.message.user .message-content td,
.message.user .message-content th {
border-color: rgba(255, 255, 255, 0.2);
}
.message.user .message-content a {
color: #bee3f8;
}
/* 工具调用和响应的样式 */
.tool-section {
margin: 12px 0;
border: 1px solid #e2e8f0;
border-radius: 6px;
overflow: hidden;
}
.tool-header {
background-color: #f8fafc;
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-weight: bold;
border-bottom: 1px solid #e2e8f0;
transition: background-color 0.2s;
}
.tool-header:hover {
background-color: #f1f5f9;
}
.tool-header .tool-icon {
transition: transform 0.2s;
}
.tool-header.collapsed .tool-icon {
transform: rotate(-90deg);
}
.tool-call .tool-header {
background-color: #eff6ff;
color: #1e40af;
}
.tool-response .tool-header {
background-color: #f0fdf4;
color: #166534;
}
.tool-content {
padding: 12px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
background-color: #f8fafc;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
}
.tool-content.collapsed {
display: none;
}
.tool-content pre {
margin: 0;
padding: 0;
background: none;
border: none;
}
.tool-name {
font-weight: bold;
color: #4a6cf7;
}
/* 用户消息中的工具样式调整 */
.message.user .tool-header {
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
}
.message.user .tool-call .tool-header {
background-color: rgba(59, 130, 246, 0.2);
}
.message.user .tool-response .tool-header {
background-color: rgba(34, 197, 94, 0.2);
}
.message.user .tool-content {
background-color: rgba(255, 255, 255, 0.05);
}
.typing-indicator {
display: flex;
align-items: center;
gap: 4px;
padding: 12px 16px;
background-color: #f1f5f9;
border-radius: 18px;
border-bottom-left-radius: 6px;
color: #64748b;
font-style: italic;
}
.typing-dots {
display: flex;
gap: 4px;
}
.typing-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #94a3b8;
animation: typing 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1) {
animation-delay: -0.32s;
}
.typing-dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input-container {
padding: 20px;
border-top: 1px solid #e0e0e0;
background-color: #fafafa;
}
.chat-input-form {
display: flex;
gap: 10px;
}
.chat-input {
flex: 1;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 24px;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
.chat-input:focus {
border-color: #4a6cf7;
}
.chat-send-button {
padding: 12px 24px;
background-color: #4a6cf7;
color: white;
border: none;
border-radius: 24px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.chat-send-button:hover {
background-color: #3b5bdb;
}
.chat-send-button:disabled {
background-color: #94a3b8;
cursor: not-allowed;
}
.settings-panel {
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
background-color: #f8fafc;
display: none;
}
.settings-panel.active {
display: block;
}
.settings-toggle {
margin-left: auto;
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
}
.settings-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.setting-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.setting-group.full-width {
grid-column: 1 / -1;
}
.setting-label {
font-size: 14px;
font-weight: bold;
color: #374151;
}
.setting-input {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
}
.setting-textarea {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
min-height: 80px;
resize: vertical;
}
.setting-checkbox {
margin-right: 8px;
}
.error-message {
padding: 10px 15px;
background-color: #fef2f2;
border: 1px solid #fecaca;
color: #dc2626;
border-radius: 6px;
margin-bottom: 15px;
}
@media (max-width: 768px) {
.chat-container {
height: 100vh;
max-width: 100%;
}
.message {
max-width: 95%;
}
.settings-form {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<div class="chat-title">AI聊天助手</div>
<div class="chat-status">
<span class="status-indicator" id="status-indicator"></span>
<span id="status-text">已连接</span>
</div>
<button class="settings-toggle" id="settings-toggle">⚙️ 设置</button>
</div>
<div class="settings-panel" id="settings-panel">
<div class="settings-form">
<div class="setting-group">
<label class="setting-label" for="model">模型</label>
<input type="text" id="model" class="setting-input" value="qwen/qwen3-next-80b-a3b-instruct">
</div>
<div class="setting-group">
<label class="setting-label" for="api-key">API Key</label>
<input type="password" id="api-key" class="setting-input" value="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">
</div>
<div class="setting-group">
<label class="setting-label" for="model-server">模型服务器</label>
<input type="text" id="model-server" class="setting-input" value="https://openrouter.ai/api/v1">
</div>
<div class="setting-group full-width">
<label class="setting-label" for="api-domain">API域名</label>
<select id="api-domain" class="setting-input">
<option value="current">当前域名</option>
<option value="https://catalog-agent-dev.gbase.ai">https://catalog-agent-dev.gbase.ai</option>
<option value="http://localhost:8001">http://localhost:8001</option>
<option value="http://qwen-agent-api:8001">http://qwen-agent-api:8001</option>
<option value="custom">自定义</option>
</select>
</div>
<div class="setting-group full-width" id="custom-api-domain-group" style="display: none;">
<label class="setting-label" for="custom-api-domain">自定义API域名</label>
<input type="text" id="custom-api-domain" class="setting-input" placeholder="https://your-api-domain.com">
</div>
<div class="setting-group">
<label class="setting-label" for="bot-id">Bot ID</label>
<input type="text" id="bot-id" class="setting-input" value="test">
</div>
<div class="setting-group">
<label class="setting-label" for="language">语言</label>
<select id="language" class="setting-input">
<option value="zh-CN" selected>中文</option>
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
</select>
</div>
<div class="setting-group">
<label class="setting-label" for="robot-type">机器人类型</label>
<select id="robot-type" class="setting-input">
<option value="">默认</option>
<option value="agent">Agent</option>
<option value="catalog_agent">Catalog Agent</option>
</select>
</div>
<div class="setting-group full-width">
<label class="setting-label" for="dataset-ids">数据集ID (逗号分隔)</label>
<input type="text" id="dataset-ids" class="setting-input" placeholder="例如: dataset1,dataset2">
</div>
<div class="setting-group full-width">
<label class="setting-label" for="mcp-settings">MCP设置 (JSON格式)</label>
<textarea id="mcp-settings" class="setting-textarea" placeholder='{"key": "value"}'></textarea>
</div>
<div class="setting-group">
<label class="setting-label" for="tool-response">
<input type="checkbox" id="tool-response" class="setting-checkbox">
启用工具响应
</label>
</div>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message assistant">
<div class="message-avatar">AI</div>
<div class="message-content">
您好我是AI助手有什么可以帮助您的吗
</div>
</div>
</div>
<div class="chat-input-container">
<form class="chat-input-form" id="chat-form">
<input
type="text"
class="chat-input"
id="chat-input"
placeholder="输入您的问题..."
autocomplete="off"
required
>
<button type="submit" class="chat-send-button" id="send-button">发送</button>
</form>
</div>
</div>
<script>
class ChatApp {
constructor() {
this.chatForm = document.getElementById('chat-form');
this.chatInput = document.getElementById('chat-input');
this.chatMessages = document.getElementById('chat-messages');
this.sendButton = document.getElementById('send-button');
this.settingsToggle = document.getElementById('settings-toggle');
this.settingsPanel = document.getElementById('settings-panel');
this.statusIndicator = document.getElementById('status-indicator');
this.statusText = document.getElementById('status-text');
this.isProcessing = false;
this.currentController = null;
this.initializeEventListeners();
this.loadSettings();
}
initializeEventListeners() {
this.chatForm.addEventListener('submit', (e) => {
e.preventDefault();
this.sendMessage();
});
this.settingsToggle.addEventListener('click', () => {
this.settingsPanel.classList.toggle('active');
});
// API域名选择变化时显示/隐藏自定义输入框
const apiDomainSelect = document.getElementById('api-domain');
const customApiDomainGroup = document.getElementById('custom-api-domain-group');
apiDomainSelect.addEventListener('change', () => {
if (apiDomainSelect.value === 'custom') {
customApiDomainGroup.style.display = 'block';
} else {
customApiDomainGroup.style.display = 'none';
}
this.saveSettings();
});
// 当用户修改设置时自动保存
const settingInputs = document.querySelectorAll('.setting-input, .setting-textarea');
settingInputs.forEach(input => {
input.addEventListener('change', () => this.saveSettings());
});
}
loadSettings() {
const settings = localStorage.getItem('chat-settings');
if (settings) {
try {
const parsedSettings = JSON.parse(settings);
Object.keys(parsedSettings).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = parsedSettings[key];
} else {
element.value = parsedSettings[key];
}
}
});
// 处理API域名设置
const apiDomainSelect = document.getElementById('api-domain');
const customApiDomainGroup = document.getElementById('custom-api-domain-group');
if (apiDomainSelect.value === 'custom') {
customApiDomainGroup.style.display = 'block';
} else {
customApiDomainGroup.style.display = 'none';
}
} catch (e) {
console.error('Failed to load settings:', e);
}
}
}
saveSettings() {
const settings = {
'model': document.getElementById('model').value,
'api-key': document.getElementById('api-key').value,
'model-server': document.getElementById('model-server').value,
'api-domain': document.getElementById('api-domain').value,
'custom-api-domain': document.getElementById('custom-api-domain').value,
'bot-id': document.getElementById('bot-id').value,
'language': document.getElementById('language').value,
'robot-type': document.getElementById('robot-type').value,
'dataset-ids': document.getElementById('dataset-ids').value,
'mcp-settings': document.getElementById('mcp-settings').value,
'tool-response': document.getElementById('tool-response').checked
};
localStorage.setItem('chat-settings', JSON.stringify(settings));
}
getSettings() {
// 获取API域名
const apiDomainSelect = document.getElementById('api-domain');
let apiUrl;
if (apiDomainSelect.value === 'current') {
apiUrl = `${window.location.origin}/api/v1/chat/completions`;
} else if (apiDomainSelect.value === 'custom') {
const customDomain = document.getElementById('custom-api-domain').value;
if (customDomain) {
// 确保自定义域名有正确的协议
let domain = customDomain.trim();
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
domain = 'https://' + domain;
}
apiUrl = `${domain}/api/v1/chat/completions`;
} else {
// 如果自定义域名为空,默认使用当前域名
apiUrl = `${window.location.origin}/api/v1/chat/completions`;
}
} else {
// 使用预定义的域名
apiUrl = `${apiDomainSelect.value}/api/v1/chat/completions`;
}
// 解析数据集ID
const datasetIds = document.getElementById('dataset-ids').value
.split(',')
.map(id => id.trim())
.filter(id => id);
// 解析MCP设置
let mcpSettings = {};
const mcpSettingsText = document.getElementById('mcp-settings').value;
if (mcpSettingsText) {
try {
mcpSettings = JSON.parse(mcpSettingsText);
} catch (e) {
console.warn('Invalid MCP settings JSON:', e);
mcpSettings = {};
}
}
return {
apiUrl: apiUrl,
model: document.getElementById('model').value,
apiKey: document.getElementById('api-key').value,
modelServer: document.getElementById('model-server').value,
botId: document.getElementById('bot-id').value,
language: document.getElementById('language').value,
robotType: document.getElementById('robot-type').value,
datasetIds: datasetIds,
mcpSettings: mcpSettings,
toolResponse: document.getElementById('tool-response').checked
};
}
updateStatus(status, text) {
this.statusIndicator.className = `status-indicator ${status}`;
this.statusText.textContent = text;
}
async sendMessage() {
if (this.isProcessing) return;
const message = this.chatInput.value.trim();
if (!message) return;
this.addUserMessage(message);
this.chatInput.value = '';
this.setProcessingState(true);
try {
await this.callAPI(message);
} catch (error) {
this.addErrorMessage(`连接错误: ${error.message}`);
this.updateStatus('disconnected', '连接失败');
} finally {
this.setProcessingState(false);
}
}
addUserMessage(content) {
const messageElement = this.createMessageElement('user', content);
this.chatMessages.appendChild(messageElement);
this.scrollToBottom();
}
addAssistantMessage(content) {
let messageElement = this.createMessageElement('assistant', '', true);
this.chatMessages.appendChild(messageElement);
this.scrollToBottom();
return messageElement;
}
addErrorMessage(content) {
const errorElement = document.createElement('div');
errorElement.className = 'error-message';
errorElement.textContent = content;
this.chatMessages.appendChild(errorElement);
this.scrollToBottom();
}
createMessageElement(role, content, isTyping = false) {
const messageContainer = document.createElement('div');
messageContainer.className = `message ${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? 'U' : 'AI';
const contentElement = document.createElement('div');
contentElement.className = 'message-content';
if (isTyping) {
contentElement.innerHTML = `
<div class="typing-indicator">
<span>AI正在思考</span>
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
`;
} else {
// 使用Markdown渲染消息内容
this.updateMessageContent(contentElement, content, role === 'user');
}
messageContainer.appendChild(avatar);
messageContainer.appendChild(contentElement);
return messageContainer;
}
setProcessingState(isProcessing) {
this.isProcessing = isProcessing;
this.sendButton.disabled = isProcessing;
this.chatInput.disabled = isProcessing;
if (isProcessing) {
this.sendButton.textContent = '发送中...';
this.updateStatus('connecting', '连接中');
} else {
this.sendButton.textContent = '发送';
this.updateStatus('', '已连接');
}
}
async callAPI(message) {
const settings = this.getSettings();
// 创建消息容器
let messageElement = this.addAssistantMessage();
let contentElement = messageElement.querySelector('.message-content');
let accumulatedContent = '';
// 创建请求体
const requestBody = {
messages: [
{
role: "user",
content: message
}
],
stream: true,
model: settings.model,
model_server: settings.modelServer,
bot_id: settings.botId,
language: settings.language,
tool_response: settings.toolResponse
};
// 添加可选参数
if (settings.robotType) {
requestBody.robot_type = settings.robotType;
}
if (settings.datasetIds && settings.datasetIds.length > 0) {
requestBody.dataset_ids = settings.datasetIds;
}
if (Object.keys(settings.mcpSettings).length > 0) {
requestBody.mcp_settings = settings.mcpSettings;
}
try {
// 使用fetch API发送请求
const response = await fetch(settings.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${settings.apiKey}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
this.updateStatus('', '已连接');
// 获取响应流
const reader = response.body.getReader();
const decoder = new TextDecoder();
// 读取流式数据
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码数据块
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
continue;
}
try {
const parsed = JSON.parse(data);
if (parsed.choices && parsed.choices[0] &&
parsed.choices[0].delta &&
parsed.choices[0].delta.content) {
const newContent = parsed.choices[0].delta.content;
accumulatedContent += newContent;
// 更新消息内容使用Markdown渲染
this.updateMessageContent(contentElement, accumulatedContent, false);
this.scrollToBottom();
}
} catch (e) {
console.error('Failed to parse chunk:', e);
}
}
}
}
// 如果没有内容,显示默认消息
if (!accumulatedContent) {
this.updateMessageContent(contentElement, '抱歉,我无法处理您的请求。', false);
}
} catch (error) {
// 如果出错移除typing指示器并显示错误
this.updateMessageContent(contentElement, `错误: ${error.message}`, false);
throw error;
}
}
renderMarkdown(content, isUserMessage = false) {
// 首先处理标签化内容
const processedContent = this.processTaggedContent(content, isUserMessage);
// 配置marked选项
marked.setOptions({
highlight: function(code, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {}
}
return hljs.highlightAuto(code).value;
},
breaks: true, // 支持换行
gfm: true // 支持GitHub风格的Markdown
});
return marked.parse(processedContent);
}
processTaggedContent(content, isUserMessage = false) {
// 直接根据标签进行切割
const parts = content.split(/\[(ANSWER|TOOL_CALL|TOOL_RESPONSE)\]/);
let processedContent = '';
let toolCallIndex = 0;
for (let i = 0; i < parts.length; i += 2) {
const tag = parts[i - 1]; // 前一个标签
const content = parts[i]; // 当前内容
if (!content || !content.trim()) continue;
if (tag === 'ANSWER') {
// 直接显示ANSWER内容
processedContent += `\n\n${content.trim()}\n\n`;
} else if (tag === 'TOOL_CALL') {
// 工具调用折叠显示
const lines = content.trim().split('\n');
const toolName = lines[0] || '未知工具';
const toolData = lines.slice(1).join('\n').trim();
const toolId = `tool-call-${toolCallIndex++}`;
processedContent += `
<div class="tool-section tool-call">
<div class="tool-header collapsed" onclick="toggleToolSection('${toolId}')">
<span class="tool-icon">▼</span>
<span>🔧 工具调用: <span class="tool-name">${toolName}</span></span>
</div>
<div class="tool-content collapsed" id="${toolId}">
<pre>${this.escapeHtml(toolData)}</pre>
</div>
</div>`;
} else if (tag === 'TOOL_RESPONSE') {
// 工具响应折叠显示
const lines = content.trim().split('\n');
const toolName = lines[0] || '未知工具响应';
const toolData = lines.slice(1).join('\n').trim();
const toolId = `tool-response-${toolCallIndex++}`;
processedContent += `
<div class="tool-section tool-response">
<div class="tool-header collapsed" onclick="toggleToolSection('${toolId}')">
<span class="tool-icon">▼</span>
<span>📋 工具响应: <span class="tool-name">${toolName}</span></span>
</div>
<div class="tool-content collapsed" id="${toolId}">
<pre>${this.escapeHtml(toolData)}</pre>
</div>
</div>`;
}
}
return processedContent;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
updateMessageContent(contentElement, content, isUserMessage = false) {
if (isUserMessage) {
// 用户消息使用简单的文本渲染但保留基本的Markdown格式
contentElement.innerHTML = this.renderSimpleMarkdown(content);
} else {
// AI助手消息使用完整的Markdown渲染
contentElement.innerHTML = this.renderMarkdown(content);
}
// 重新高亮代码块
contentElement.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
renderSimpleMarkdown(content) {
// 为用户消息提供简单的Markdown渲染
return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // 粗体
.replace(/\*(.*?)\*/g, '<em>$1</em>') // 斜体
.replace(/`([^`]+)`/g, '<code>$1</code>') // 行内代码
.replace(/\n/g, '<br>'); // 换行
}
scrollToBottom() {
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
}
}
// 全局函数:切换工具内容的折叠状态
function toggleToolSection(toolId) {
const header = document.querySelector(`[onclick="toggleToolSection('${toolId}')"]`);
const content = document.getElementById(toolId);
if (header && content) {
header.classList.toggle('collapsed');
content.classList.toggle('collapsed');
}
}
// 初始化聊天应用
document.addEventListener('DOMContentLoaded', () => {
new ChatApp();
});
</script>
</body>
</html>