Gem2a / app /static /js /chat.js
misonL's picture
Modularize frontend CSS and JavaScript assets
19bd286
Raw
History Blame Contribute Delete
10.4 kB
// app/static/js/chat.js - 聊天功能相关逻辑
import { AUTH_TOKEN_STORAGE_KEY, CURRENT_SESSION_KEY } from './constants.js';
import { fileToBase64, extractContent } from './utils.js';
/**
* 将消息添加到聊天窗口。
* @param {string} sender - 消息发送者 ('user', 'bot', 'system')。
* @param {string} text - 消息文本内容。
* @param {string|null} userProvidedImageUrl - 用户提供的图片URL(可选)。
* @param {string[]} botImageUrls - Bot 返回的图片URL数组(可选)。
*/
export function addMessageToChat(sender, text, userProvidedImageUrl = null, botImageUrls = []) {
const chatWindow = document.getElementById('chat-window');
const messageElement = document.createElement('div');
messageElement.classList.add('message', sender);
// 如果是用户发送的图片,直接显示
if (userProvidedImageUrl) {
const imgElement = document.createElement('img');
imgElement.src = userProvidedImageUrl;
imgElement.style.maxWidth = '200px';
imgElement.style.display = 'block';
imgElement.style.marginTop = '5px';
imgElement.alt = '用户上传图片';
messageElement.appendChild(imgElement);
}
let processedText = text;
let extractedMermaidCodes = [];
// 如果是 bot 消息,先添加图片,再处理文本和 Mermaid
if (sender === 'bot') {
// 提取 Mermaid 代码、清理文本
const result = extractContent(text);
processedText = result.cleanedText;
extractedMermaidCodes = result.mermaidCodes;
const allImageUrls = [...botImageUrls];
// 添加所有提取到的图片
if (allImageUrls.length > 0) {
const authToken = localStorage.getItem(AUTH_TOKEN_STORAGE_KEY);
allImageUrls.forEach(imgUrl => {
const imgElement = document.createElement('img');
imgElement.style.maxWidth = '200px';
imgElement.style.display = 'block';
imgElement.style.marginTop = '5px';
imgElement.alt = 'Gemini 输出图片';
// 添加错误处理,如果图片加载失败则显示替代文本
imgElement.onerror = () => {
console.error(`图片加载失败: ${imgUrl}`);
imgElement.alt = `图片加载失败: ${imgUrl}`;
imgElement.src = '';
};
// 检查是否是代理 URL
if (imgUrl.startsWith('/v1/image_proxy?image_url=')) {
if (authToken) {
// 使用 fetch API 发送带有认证头的请求
fetch(imgUrl, {
headers: {
'X-Auth-Token': authToken
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
})
.then(imageBlob => {
imgElement.src = URL.createObjectURL(imageBlob);
messageElement.appendChild(imgElement);
})
.catch(error => {
console.error(`代理图片加载失败: ${imgUrl}`, error);
imgElement.onerror();
});
} else {
console.error('未找到认证令牌,无法加载代理图片。');
imgElement.onerror();
}
} else {
// 直接使用 imgUrl,因为后端现在可能返回 Base64 Data URL
imgElement.src = imgUrl;
messageElement.appendChild(imgElement);
}
});
}
}
// 渲染剩余的文本内容(包括 Markdown 和非图片/Mermaid 文本)
if (processedText) {
const textElement = document.createElement('div');
textElement.innerHTML = marked.parse(processedText);
messageElement.appendChild(textElement);
}
chatWindow.appendChild(messageElement);
chatWindow.scrollTop = chatWindow.scrollHeight;
// 在消息元素添加到 DOM 后,再渲染 Mermaid 图表
extractedMermaidCodes.forEach((mermaidCode, index) => {
const mermaidDiv = document.createElement('div');
mermaidDiv.classList.add('mermaid');
// 为每个 Mermaid 图表生成一个唯一的 ID
const chartId = `mermaid-chart-${Date.now()}-${index}`;
mermaidDiv.id = chartId;
messageElement.appendChild(mermaidDiv);
// 使用 mermaid.render 方法直接渲染 Mermaid 代码
mermaid.render(chartId, mermaidCode)
.then(({ svg, bindFunctions }) => {
const renderedMermaidDiv = document.getElementById(chartId);
if (renderedMermaidDiv) {
renderedMermaidDiv.innerHTML = svg;
if (bindFunctions) {
bindFunctions(renderedMermaidDiv);
}
}
})
.catch(error => {
console.error(`Mermaid 渲染失败 (ID: ${chartId}):`, error);
const renderedMermaidDiv = document.getElementById(chartId);
if (renderedMermaidDiv) {
renderedMermaidDiv.innerHTML = `<div style="color: red;">Mermaid 渲染错误: ${error.message || '语法错误'}</div>`;
}
});
});
}
/**
* 设置发送消息按钮的事件监听器。
* @param {HTMLButtonElement} sendButton - 发送按钮元素。
* @param {HTMLInputElement} userInput - 用户输入文本框。
* @param {HTMLInputElement} imageUpload - 图片上传输入框。
* @param {HTMLSelectElement} modelSelect - 模型选择器。
*/
export function setupSendButton(sendButton, userInput, imageUpload, modelSelect) {
if (sendButton) {
sendButton.addEventListener('click', async () => {
const authToken = localStorage.getItem(AUTH_TOKEN_STORAGE_KEY);
const textInput = userInput.value.trim();
const imageFile = imageUpload.files[0];
const selectedModel = modelSelect ? modelSelect.value : "gemini-2.5-flash";
if (!authToken) {
alert('认证令牌未找到,请重新登录。');
window.location.href = '/static/login.html';
return;
}
if (!textInput && !imageFile) {
alert('请输入文本或上传图片。');
return;
}
sendButton.disabled = true;
sendButton.textContent = '发送中...';
let imageUrlForDisplay = null;
let base64ImageForRequest = null;
if (imageFile) {
base64ImageForRequest = await fileToBase64(imageFile);
imageUrlForDisplay = `data:${imageFile.type};base64,${base64ImageForRequest}`;
}
const threadId = localStorage.getItem(CURRENT_SESSION_KEY) || '';
addMessageToChat('user', textInput, imageUrlForDisplay);
userInput.value = '';
imageUpload.value = '';
const requestBody = {
model: selectedModel,
messages: [{ role: "user", content: textInput }],
thread_id: threadId
};
if (base64ImageForRequest) {
requestBody.image_data = base64ImageForRequest;
requestBody.image_mime_type = imageFile.type;
}
try {
const response = await fetch('/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': authToken
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const responseData = await response.json();
const botMessageContent = responseData.choices[0]?.message?.content || "无响应内容";
const botImageUrls = responseData.choices[0]?.message?.image_urls || [];
addMessageToChat('bot', botMessageContent, null, botImageUrls);
} else {
const errorData = await response.json();
addMessageToChat('system', `请求失败: ${errorData.detail || response.statusText}`);
}
} catch (error) {
addMessageToChat('system', `请求出错: ${error.message}`);
} finally {
sendButton.disabled = false;
sendButton.textContent = '发送';
}
});
}
}
/**
* 实现从剪切板粘贴图片的功能。
* @param {HTMLInputElement} userInput - 用户输入文本框。
* @param {HTMLInputElement} imageUpload - 图片上传输入框。
*/
export function setupPasteImage(userInput, imageUpload) {
if (userInput) {
userInput.addEventListener('paste', (event) => {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (const item of items) {
if (item.kind === 'file' && item.type.startsWith('image/')) {
const blob = item.getAsFile();
const dataTransfer = new DataTransfer();
dataTransfer.items.add(blob);
imageUpload.files = dataTransfer.files;
const reader = new FileReader();
reader.onload = (e) => {
addMessageToChat('user', '图片已粘贴,等待发送...', e.target.result);
console.log('图片已从剪切板粘贴,准备上传。');
};
reader.readAsDataURL(blob);
event.preventDefault();
break;
}
}
});
}
}