G_AI / app /frontend /script.js
superxu520's picture
"fix_deep_stream_interrupted_and_timeout"
378caa6
const API_BASE_URL = '';
const CHAT_ENDPOINT = `${API_BASE_URL}/v1/chat/completions`;
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const newChatBtn = document.getElementById('new-chat-btn');
const fileInput = document.getElementById('file-input');
const uploadBtn = document.getElementById('upload-btn');
const previewContainer = document.getElementById('preview-container');
const drawModeSwitch = document.getElementById('draw-mode-switch');
const healthDot = document.getElementById('health-dot');
let currentAbortController = null;
function updateUIState(typing) {
isTyping = typing;
if (typing) {
sendBtn.classList.remove('disabled');
sendBtn.classList.add('stop-mode');
sendBtn.innerHTML = '<i class="fas fa-stop"></i>';
sendBtn.title = '停止生成';
} else {
const text = userInput.value.trim();
sendBtn.classList.toggle('disabled', !text && attachedFiles.length === 0);
sendBtn.classList.remove('stop-mode');
sendBtn.innerHTML = '<i class="fas fa-paper-plane"></i>';
sendBtn.title = '发送';
}
}
async function checkHealth() {
if (!healthDot) return;
try {
const response = await fetch(`${API_BASE_URL}/health`);
const data = await response.json();
if (data && data.ok) {
healthDot.className = 'status-dot online';
healthDot.title = '后端服务正常';
} else {
healthDot.className = 'status-dot offline';
healthDot.title = '后端服务异常';
}
} catch (error) {
healthDot.className = 'status-dot offline';
healthDot.title = '连接失败';
}
}
// 检测是否为移动设备
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
// 画图/搜图意图识别关键词
const DRAW_KEYWORDS = [
'画', '图', '生成图片', '生成图像', '画一张', '画个', '画一',
'搜索', '搜', '搜图', '查找图片', '找图片',
'image', 'draw', 'paint', 'illustration', 'picture', 'photo', 'search'
];
function isDrawingRequest(text) {
if (!text) return false;
const lowerText = text.toLowerCase();
return DRAW_KEYWORDS.some(key => lowerText.includes(key.toLowerCase()));
}
// 压缩历史记录中的图片:移除 base64 编码的图片数据以减小请求体积
function compressHistoryForAPI(history) {
return history.map(msg => {
if (typeof msg.content === 'string') {
return {
role: msg.role,
content: msg.content.replace(/!\[([^\]]*)\]\(data:image\/[^;]+;base64,[A-Za-z0-9+/=]+\)/g, '')
};
}
if (Array.isArray(msg.content)) {
return {
role: msg.role,
content: msg.content.filter(part => {
if (part.type === 'image_url' && part.image_url.url.startsWith('data:')) {
return false; // 提示词中移除大的 base64 图片
}
return true;
})
};
}
return msg;
});
}
let chatHistory = [];
let attachedFiles = []; // 存储当前待发送的文件
let isTyping = false;
// 配置 marked 自定义渲染器
const renderer = new marked.Renderer();
// 语言显示名称映射
const LANG_DISPLAY_NAMES = {
'plaintext': '文本',
'text': '文本',
'python': 'Python',
'py': 'Python',
'javascript': 'JavaScript',
'js': 'JavaScript',
'typescript': 'TypeScript',
'ts': 'TypeScript',
'java': 'Java',
'cpp': 'C++',
'c': 'C',
'csharp': 'C#',
'cs': 'C#',
'go': 'Go',
'rust': 'Rust',
'rs': 'Rust',
'ruby': 'Ruby',
'rb': 'Ruby',
'php': 'PHP',
'swift': 'Swift',
'kotlin': 'Kotlin',
'kt': 'Kotlin',
'scala': 'Scala',
'r': 'R',
'matlab': 'MATLAB',
'sql': 'SQL',
'bash': 'Bash',
'sh': 'Shell',
'shell': 'Shell',
'powershell': 'PowerShell',
'ps': 'PowerShell',
'html': 'HTML',
'css': 'CSS',
'scss': 'SCSS',
'sass': 'Sass',
'less': 'Less',
'xml': 'XML',
'json': 'JSON',
'yaml': 'YAML',
'yml': 'YAML',
'toml': 'TOML',
'ini': 'INI',
'dockerfile': 'Dockerfile',
'docker': 'Dockerfile',
'makefile': 'Makefile',
'markdown': 'Markdown',
'md': 'Markdown',
'latex': 'LaTeX',
'tex': 'LaTeX',
'regex': 'Regex'
};
renderer.code = function (code, lang) {
const rawLang = (lang || 'plaintext').toLowerCase().trim();
const displayName = LANG_DISPLAY_NAMES[rawLang] || rawLang.toUpperCase();
// 使用 highlight.js 进行高亮处理
const highlighted = hljs.getLanguage(rawLang)
? hljs.highlight(code, { language: rawLang }).value
: hljs.highlightAuto(code).value;
return `
<div class="code-block-wrapper">
<div class="code-header">
<span class="code-lang">${displayName}</span>
<button class="copy-code-btn" onclick="copyToClipboard(this)">
<i class="far fa-copy"></i> 复制
</button>
</div>
<pre><code class="hljs language-${rawLang}">${highlighted}</code></pre>
</div>
`;
};
marked.setOptions({
renderer: renderer,
breaks: true,
gfm: true,
headerIds: false,
mangle: false
});
// 全局复制函数
window.copyToClipboard = (btn) => {
const code = btn.closest('.code-block-wrapper').querySelector('code').innerText;
navigator.clipboard.writeText(code).then(() => {
const originalHtml = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> 已复制';
btn.classList.add('success');
setTimeout(() => {
btn.innerHTML = originalHtml;
btn.classList.remove('success');
}, 2000);
}).catch(err => {
console.error('复制失败:', err);
// Fallback for mobile
const textArea = document.createElement('textarea');
textArea.value = code;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
btn.innerHTML = '<i class="fas fa-check"></i> 已复制';
btn.classList.add('success');
setTimeout(() => {
btn.innerHTML = originalHtml;
btn.classList.remove('success');
}, 2000);
} catch (e) {
console.error('复制失败:', e);
}
document.body.removeChild(textArea);
});
};
// 自动调整输入框高度
function adjustTextareaHeight() {
userInput.style.height = 'auto';
const maxHeight = isMobile ? 120 : 200;
const newHeight = Math.min(userInput.scrollHeight, maxHeight);
userInput.style.height = newHeight + 'px';
}
userInput.addEventListener('input', () => {
adjustTextareaHeight();
sendBtn.classList.toggle('disabled', !userInput.value.trim() && attachedFiles.length === 0);
});
// 聊天记录持久化管理
function saveHistory() {
try {
localStorage.setItem('gemini_chat_history', JSON.stringify(chatHistory));
} catch (e) {
console.warn('无法保存历史记录:', e);
}
}
function loadHistory() {
try {
const saved = localStorage.getItem('gemini_chat_history');
if (saved) {
chatHistory = JSON.parse(saved);
if (chatHistory.length > 0) {
chatContainer.innerHTML = '';
chatHistory.forEach(msg => appendMessage(msg.role, msg.content));
scrollToBottom();
}
}
} catch (e) {
console.warn('无法加载历史记录:', e);
chatHistory = [];
}
}
function scrollToBottom() {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// 模型选择逻辑(固定为 gemini-3.0-pro)
let currentModel = 'gemini-3.0-pro';
const currentModelNameSpan = document.getElementById('current-model-name');
if (currentModelNameSpan) {
currentModelNameSpan.textContent = 'Gemini 3 Pro';
}
// 修改发送消息逻辑以包含模型参数
async function sendMessage() {
// 如果正在输入,点击按钮则停止生成
if (isTyping) {
if (currentAbortController) {
currentAbortController.abort();
}
return;
}
const text = userInput.value.trim();
if (!text && attachedFiles.length === 0) return;
// 创建新的中断控制器
currentAbortController = new AbortController();
const signal = currentAbortController.signal;
if (chatContainer.querySelector('.welcome-screen')) chatContainer.innerHTML = '';
// 1. 构建显示内容
let userDisplayContent = text ? escapeHtml(text) : '';
if (attachedFiles.length > 0) {
const imagesHtml = attachedFiles
.filter(f => f.type.startsWith('image/'))
.map(f => `<img src="${f.data}" style="max-width: 100%; max-height: 280px; border-radius: 8px; margin-top: 10px; display: block;">`)
.join('');
const filesHtml = attachedFiles
.filter(f => !f.type.startsWith('image/'))
.map(f => `<div style="background: var(--bg-tertiary); padding: 8px 12px; border-radius: 8px; margin-top: 6px; font-size: 13px; display: inline-flex; align-items: center; gap: 8px;"><i class="fas fa-file"></i> ${escapeHtml(f.name)}</div>`)
.join('');
userDisplayContent += (imagesHtml ? '<br>' + imagesHtml : '') + filesHtml;
}
appendMessage('user', userDisplayContent);
// 2. 构建 API 消息格式
const messageParts = [];
if (text) messageParts.push({ type: 'text', text: text });
attachedFiles.forEach(file => {
messageParts.push({
type: 'image_url',
image_url: { url: `data:${file.type};base64,${file.base64}` }
});
});
// 3. 清空输入状态
userInput.value = '';
userInput.style.height = 'auto';
attachedFiles = [];
renderPreviews();
updateUIState(true);
// 4. 更新历史记录 (保存结构化内容以保留图片等上下文)
chatHistory.push({ role: 'user', content: messageParts });
saveHistory();
const contentDiv = appendMessage('assistant', '', 'ai-' + Date.now());
let aiText = '';
try {
const API_KEY = window.GEMINI_CONFIG?.API_KEY || '';
let response;
const isDrawMode = drawModeSwitch.checked;
if (isDrawMode) {
// 1. 画图模式:使用特殊的 /v1/responses 接口和格式
// 构建带历史记录的输入
const compressedHistory = compressHistoryForAPI(chatHistory.slice(0, -1));
const historyContext = compressedHistory.map(m => `${m.role}: ${m.content}`).join('\n');
const inputWithHistory = historyContext ? `${historyContext}\nuser: ${text}` : text;
response = await fetch(`${API_BASE_URL}/v1/responses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: 'gemini-3.0-pro',
input: inputWithHistory,
tools: [{ type: 'image_generation' }],
temperature: 0.9
}),
signal: signal
});
if (!response.ok) throw new Error(`画图请求失败: ${response.status}`);
const result = await response.json();
let finalMarkdown = '';
// 解析响应:提取文字和图片URL
if (result.output && Array.isArray(result.output)) {
result.output.forEach(item => {
if (item.type === 'message' && item.content) {
item.content.forEach(c => {
if (c.type === 'output_text') finalMarkdown += c.text;
});
}
});
}
if (finalMarkdown) {
contentDiv.innerHTML = marked.parse(finalMarkdown);
chatHistory.push({ role: 'assistant', content: finalMarkdown });
} else {
contentDiv.innerHTML = "未能提取到有效响应";
}
updateUIState(false);
saveHistory();
return; // 结束画图逻辑
}
// 2. 普通对话模式:自动识别画图意图
const autoDetectDraw = isDrawingRequest(text);
// 使用压缩后的历史记录(移除 base64 图片数据)
const compressedHistory = compressHistoryForAPI(chatHistory.slice(0, -1));
response = await fetch(CHAT_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: currentModel,
messages: [
...compressedHistory,
{ role: 'user', content: messageParts }
],
tools: autoDetectDraw ? [{
type: 'function',
function: {
name: 'image_generation',
description: 'Generate an image based on user prompt',
parameters: {
type: 'object',
properties: {
prompt: { type: 'string' }
},
required: ['prompt']
}
}
}] : [],
stream: !autoDetectDraw // 检测到画图意图时关闭流式输出
}),
signal: signal
});
if (!response.ok) {
if (response.status === 502 || response.status === 504) {
throw new Error('上游服务(Gemini)响应超时或连接中断,请稍后重试或更换图片。');
} else if (response.status === 503) {
throw new Error('服务器当前繁忙,请稍后重试。');
}
throw new Error(`API 请求失败: ${response.status}`);
}
// 如果检测到画图意图,使用非流式处理
if (autoDetectDraw) {
const result = await response.json();
const message = result.choices[0].message;
if (message.tool_calls && message.tool_calls.length > 0) {
await handleToolCalls(message.tool_calls, contentDiv, API_KEY);
} else if (message.content) {
contentDiv.innerHTML = marked.parse(message.content);
chatHistory.push({ role: 'assistant', content: message.content });
}
updateUIState(false);
saveHistory();
return;
}
// 渲染文并处理代码块
const renderText = (text) => {
let processed = text;
const codeBlockCount = (processed.match(/```/g) || []).length;
if (codeBlockCount % 2 !== 0) {
if (!processed.endsWith('\n')) processed += '\n';
processed += '```';
}
contentDiv.innerHTML = marked.parse(processed);
contentDiv.querySelectorAll('pre code').forEach((block) => hljs.highlightElement(block));
};
// 普通流式处理
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let streamedToolCalls = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = (buffer + chunk).split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim().startsWith('data: ')) {
const data = line.trim().slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
// --- OpenAI 格式处理 ---
if (parsed.choices && parsed.choices[0]) {
const choice = parsed.choices[0];
const delta = choice.delta;
if (delta.content) {
aiText += delta.content;
renderText(aiText);
}
if (delta.tool_calls) {
for (const tcall of delta.tool_calls) {
if (!streamedToolCalls[tcall.index]) {
streamedToolCalls[tcall.index] = {
id: tcall.id,
type: tcall.type || 'function',
function: { name: '', arguments: '' }
};
}
if (tcall.function) {
if (tcall.function.name) streamedToolCalls[tcall.index].function.name += tcall.function.name;
if (tcall.function.arguments) streamedToolCalls[tcall.index].function.arguments += tcall.function.arguments;
}
}
}
}
// --- Anthropic 格式处理 ---
else if (parsed.type) {
if (parsed.type === 'content_block_delta' && parsed.delta) {
if (parsed.delta.type === 'text_delta') {
aiText += parsed.delta.text;
renderText(aiText);
} else if (parsed.delta.type === 'input_json_delta') {
const idx = parsed.index;
if (!streamedToolCalls[idx]) streamedToolCalls[idx] = { function: { name: '', arguments: '' } };
streamedToolCalls[idx].function.arguments += parsed.delta.partial_json;
}
} else if (parsed.type === 'content_block_start' && parsed.content_block) {
if (parsed.content_block.type === 'tool_use') {
const idx = parsed.index;
streamedToolCalls[idx] = {
id: parsed.content_block.id,
function: { name: parsed.content_block.name, arguments: '' }
};
}
}
}
if (!isMobile || Math.random() > 0.7) scrollToBottom();
} catch (e) { }
}
}
}
// 流结束后检查是否有工具调用需要处理
if (streamedToolCalls.length > 0) {
await handleToolCalls(streamedToolCalls.filter(c => c), contentDiv, API_KEY);
} else {
chatHistory.push({ role: 'assistant', content: aiText });
}
updateUIState(false);
saveHistory();
} catch (error) {
if (error.name === 'AbortError') {
console.log('生成已停止');
if (!aiText) contentDiv.innerHTML = "<em>生成已手动停止</em>";
} else {
console.error('Error:', error);
contentDiv.innerHTML = marked.parse(`抱歉,出错了:${error.message}`);
}
updateUIState(false);
} finally {
isTyping = false;
scrollToBottom();
}
}
// 统一的工具调用处理逻辑
async function handleToolCalls(toolCalls, contentDiv, API_KEY) {
if (!toolCalls || toolCalls.length === 0) return;
// 目前逻辑主要处理搜图和画图。如果有多个工具调用,我们优先处理第一个图像相关的。
const toolCall = toolCalls.find(c => c.function.name === 'image_generation' || c.function.name === 'image_retrieval:search') || toolCalls[0];
const toolName = toolCall.function.name;
// 仅自动处理画图和搜图工具
if (toolName === 'image_generation' || toolName === 'image_retrieval:search') {
try {
const args = JSON.parse(toolCall.function.arguments);
// 兼容不同工具的参数名:画图用 prompt,搜图用 query
const prompt = args.prompt || args.query || "";
const actionName = toolName === 'image_generation' ? '生成' : '搜索';
if (!prompt) return;
contentDiv.innerHTML = `<div class="typing-indicator"><span></span><span></span><span></span></div><div style="margin-top: 8px;">正在为你${actionName}图片:<strong>${escapeHtml(prompt)}</strong>...</div>`;
const imageResponse = await fetch(`${API_BASE_URL}/v1/responses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: 'gemini-3.0-pro',
input: prompt,
tools: [{ type: 'image_generation' }],
temperature: 0.9
})
});
if (!imageResponse.ok) throw new Error('服务响应异常');
const imageResult = await imageResponse.json();
let finalMarkdown = '';
if (imageResult.output && Array.isArray(imageResult.output)) {
imageResult.output.forEach(item => {
if (item.type === 'message' && item.content) {
item.content.forEach(c => {
if (c.type === 'output_text') finalMarkdown += c.text;
});
}
});
}
if (finalMarkdown) {
contentDiv.innerHTML = marked.parse(finalMarkdown);
chatHistory.push({ role: 'assistant', content: finalMarkdown });
saveHistory();
} else {
contentDiv.innerHTML = "未能获取到图片内容";
}
} catch (error) {
console.error('工具调用处理失败:', error);
contentDiv.innerHTML = marked.parse(`操作失败:${escapeHtml(error.message)}`);
}
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function appendMessage(role, text, id = null) {
const msgDiv = document.createElement('div');
msgDiv.className = `message ${role}`;
if (id) msgDiv.id = id;
const avatar = document.createElement('div');
avatar.className = 'avatar';
if (role === 'user') {
avatar.innerHTML = '<i class="fas fa-user"></i>';
} else {
avatar.innerHTML = '<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzRhOTBmNSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPSJNMTIgM2MtNC45NyAwLTkgNC4wMy05IDlzNC4wMyA5IDkgOSA5LTQuMDMgOS05LTQuMDMtOS05LTl6bTAgMTRjLTIuNzYgMC01LTIuMjQtNS01czIuMjQtNSA1LTUgNSAyLjI0IDUgNS0yLjI0IDUtNSA1em0wLTljLTEuNjYgMC0zIDEuMzQtMyAzczEuMzQgMyAzIDMgMy0xLjM0IDMtMy0xLjM0LTMtMy0zeiIvPjwvc3ZnPg==">';
}
const content = document.createElement('div');
content.className = 'content';
if (!text) {
content.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
} else {
content.innerHTML = text.startsWith('<') ? text : marked.parse(text);
// 高亮代码块
setTimeout(() => {
content.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}, 0);
}
msgDiv.appendChild(avatar);
msgDiv.appendChild(content);
chatContainer.appendChild(msgDiv);
scrollToBottom();
return content;
}
// 事件绑定
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
// 监听粘贴事件
userInput.addEventListener('paste', (e) => {
const items = e.clipboardData.items;
for (let item of items) {
if (item.type.indexOf('image') !== -1) {
e.preventDefault();
const file = item.getAsFile();
handleFiles([file]);
}
}
});
function handleFiles(files) {
Array.from(files).forEach(file => {
if (attachedFiles.length >= 5) {
alert('最多只能上传 5 个文件');
return;
}
// 检查文件大小 (最大 10MB)
if (file.size > 10 * 1024 * 1024) {
alert(`文件 ${file.name} 超过 10MB 限制`);
return;
}
if (file.type.startsWith('image/')) {
// 图片压缩处理
compressImage(file, 1024, 0.8).then(compressed => {
const fileObj = {
name: file.name,
type: compressed.type,
data: compressed.dataUrl,
base64: compressed.base64
};
attachedFiles.push(fileObj);
renderPreviews();
}).catch(err => {
console.error('图片压缩失败:', err);
// 压缩失败则尝试原样读取
readOriginalFile(file);
});
} else {
readOriginalFile(file);
}
});
// 重置 input 以便可以重复选择同一文件
fileInput.value = '';
}
function readOriginalFile(file) {
const reader = new FileReader();
reader.onload = (e) => {
const fileObj = {
name: file.name,
type: file.type,
data: e.target.result,
base64: e.target.result.split(',')[1]
};
attachedFiles.push(fileObj);
renderPreviews();
};
reader.onerror = () => {
alert(`读取文件 ${file.name} 失败`);
};
reader.readAsDataURL(file);
}
/**
* 压缩图片
* @param {File} file 原始文件
* @param {number} maxSide 最大边长
* @param {number} quality 压缩质量 (0-1)
*/
function compressImage(file, maxSide, quality) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
const img = new Image();
img.src = e.target.result;
img.onload = () => {
const canvas = document.createElement('canvas');
let width = img.width;
let height = img.height;
// 计算缩放比例
if (width > height) {
if (width > maxSide) {
height = Math.round((height * maxSide) / width);
width = maxSide;
}
} else {
if (height > maxSide) {
width = Math.round((width * maxSide) / height);
height = maxSide;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
// 导出为 JPEG (体积更小)
const dataUrl = canvas.toDataURL('image/jpeg', quality);
const base64 = dataUrl.split(',')[1];
resolve({
dataUrl: dataUrl,
base64: base64,
type: 'image/jpeg'
});
};
img.onerror = reject;
};
reader.onerror = reject;
});
}
function renderPreviews() {
previewContainer.innerHTML = '';
attachedFiles.forEach((file, index) => {
const item = document.createElement('div');
item.className = 'preview-item';
if (file.type.startsWith('image/')) {
item.innerHTML = `<img src="${file.data}" alt=""><button onclick="removeFile(${index})" aria-label="删除"><i class="fas fa-times"></i></button>`;
} else {
const ext = file.name.split('.').pop().toUpperCase();
item.innerHTML = `<div class="file-icon"><i class="fas fa-file-alt"></i><span>${ext}</span></div><button onclick="removeFile(${index})" aria-label="删除"><i class="fas fa-times"></i></button>`;
}
previewContainer.appendChild(item);
});
sendBtn.classList.toggle('disabled', !userInput.value.trim() && attachedFiles.length === 0);
}
window.removeFile = (index) => {
attachedFiles.splice(index, 1);
renderPreviews();
};
sendBtn.addEventListener('click', sendMessage);
// 键盘事件处理
userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// 移动端优化:防止键盘弹起时布局问题
if (isMobile) {
userInput.addEventListener('focus', () => {
setTimeout(() => {
scrollToBottom();
}, 300);
});
}
// 新对话按钮
newChatBtn.addEventListener('click', () => {
if (isTyping) return;
if (chatHistory.length > 0 && !confirm('确定要开始新对话吗?当前对话记录将被清除。')) {
return;
}
chatHistory = [];
localStorage.removeItem('gemini_chat_history');
attachedFiles = [];
renderPreviews();
chatContainer.innerHTML = `
<div class="welcome-screen">
<h1>你好,我是你的 AI 助手</h1>
<p>我可以帮你写作、编码、学习或提供创意建议。有什么可以帮你的吗?</p>
<div class="suggested-cards">
<div class="card" data-prompt="帮我写一段 Python 爬虫代码">
<i class="fas fa-code" style="color: var(--accent-blue); margin-bottom: 8px; display: block;"></i>
帮我写一段 Python 爬虫代码
</div>
<div class="card" data-prompt="解释量子力学的基础概念">
<i class="fas fa-atom" style="color: var(--accent-purple); margin-bottom: 8px; display: block;"></i>
解释量子力学的基础概念
</div>
<div class="card" data-prompt="策划一次 3 天的上海旅行">
<i class="fas fa-plane" style="color: #4ade80; margin-bottom: 8px; display: block;"></i>
策划一次 3 天的上海旅行
</div>
<div class="card" data-prompt="画一张未来城市的夜景">
<i class="fas fa-paint-brush" style="color: #f472b6; margin-bottom: 8px; display: block;"></i>
画一张未来城市的夜景
</div>
</div>
</div>
`;
bindCardEvents();
checkHealth(); // 新对话时也检查一次
});
// 建议卡片点击事件
function bindCardEvents() {
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('click', () => {
const prompt = card.getAttribute('data-prompt');
if (prompt) {
userInput.value = prompt;
userInput.style.height = 'auto';
userInput.style.height = userInput.scrollHeight + 'px';
sendBtn.classList.remove('disabled');
userInput.focus();
// 移动端自动发送
if (isMobile) {
sendMessage();
}
}
});
});
}
// 初始化界面
loadHistory();
renderPreviews();
bindCardEvents();
checkHealth(); // 首次打开页面检查一次
// 处理窗口大小变化
window.addEventListener('resize', () => {
scrollToBottom();
});