Moonfanz's picture
Upload 2 files
557248d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom-Gemini</title>
<link href="{{ url_for('static', filename='css/output.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/dracula.css') }}" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js"></script>
<script src="{{ url_for('static', filename='js/clipboard.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/markdown-it.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/markdown-renderer.js') }}"></script>
<style>
/* Inline CSS */
.left-controls {
position: absolute;
left: 20px;
top: 76px;
width: 250px;
}
.code-block {
margin-top: 0px;
}
.chat-container {
position: relative;
max-width: 3xl;
margin: 0 auto;
height: calc(100vh - 40px);
display: flex;
flex-direction: column;
}
#chat-history {
flex-grow: 1;
overflow-y: auto;
}
#user-input {
min-height: 80px;
max-height: 120px;
resize: none;
overflow-y: auto;
}
#user-input::-webkit-scrollbar,
#chat-history::-webkit-scrollbar {
display: none;
}
#chat-history {
max-width: 100%;
word-wrap: break-word;
overflow-wrap: break-word;
}
#chat-history pre {
max-width: 100%;
white-space: pre-wrap;
overflow-x: auto;
}
#chat-history code {
white-space: pre-wrap;
word-break: break-all;
}
#file-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 10px;
min-height: 50px;
}
.file-item {
position: relative;
width: 100px;
height: 100px;
border-radius: 8px;
overflow: hidden;
border: 2px solid #e5e7eb;
background: #f9fafb;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.delete-button {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
border: none;
transition: background 0.2s;
}
.delete-button:hover {
background: rgba(0, 0, 0, 0.7);
}
.file-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 10px;
font-size: 8px;
transform: translateY(100%);
transition: transform 0.2s;
}
.file-item:hover .file-info {
transform: translateY(0);
}
/* 美化文件上传按钮 */
.file-upload-container {
position: relative;
width: 48px;
height: 48px;
margin-right: 8px;
}
.file-upload-button {
width: 100%;
height: 100%;
background: #3b82f6;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
}
.file-upload-button:hover {
background: #2563eb;
}
.file-upload-button::before {
content: "+";
color: white;
font-size: 24px;
}
#fileInput {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.upload-status {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.upload-status.active {
opacity: 1;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #ffffff;
border-top: 3px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.success-icon {
color: #10B981;
font-size: 24px;
}
.error-icon {
color: #EF4444;
font-size: 24px;
}
.upload-error-message {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(239, 68, 68, 0.9);
color: white;
padding: 4px;
font-size: 12px;
text-align: center;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gradient-to-br from-purple-100 to-blue-100 min-h-screen font-['Inter']">
<div class="left-controls container">
<div class="item">
<select id="preset-selector"
class="p-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-300 focus:outline-none">
</select>
</div>
<div class="item">
<button id="add-preset-btn"
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg transition duration-300 ease-in-out">
添加新预设
</button>
</div>
<div class="item">
<button id="delete-preset-btn"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition duration-300 ease-in-out">
删除该预设
</button>
</div>
</div>
<div class="flex justify-center items-start min-h-screen">
<div class="chat-container container p-4 max-w-3xl flex flex-col h-screen">
<h1 class="text-3xl font-bold mb-6 text-center text-gray-800">Custom-Gemini</h1>
<div id="chat-history" class="bg-white p-6 rounded-xl shadow-lg mb-6 flex-grow overflow-y-auto">
</div>
<div class="flex flex-col shadow-lg rounded-xl overflow-hidden min-h-[80px]">
<div id="file-preview" class="flex-row"></div>
<div class="flex flex-row rounded-xl">
<textarea id="user-input"
class="flex-grow p-4 border-none focus:ring-2 focus:ring-blue-300 focus:outline-none resize-y"
placeholder="输入您的消息..."></textarea>
<div class="flex items-center px-2 bg-white">
<div class="file-upload-container">
<div class="file-upload-button"></div>
<input type="file" id="fileInput" multiple onChange="handleFileUpload(event)">
</div>
<button id="send-btn"
class="bg-blue-500 hover:bg-blue-600 text-white w-12 h-12 rounded-lg transition duration-300 ease-in-out flex items-center justify-center">
发送
</button>
</div>
</div>
</div>
<button id="clear-btn"
class="mt-6 bg-red-500 hover:bg-red-600 text-white p-3 rounded-xl w-full transition duration-300 ease-in-out">
清除聊天记录
</button>
</div>
</div>
<script>
const defaultPresets = ["默认", "文章润色", "翻译", "辩论"];
const chatHistory = document.getElementById('chat-history');
const userInput = document.getElementById('user-input');
const filePreview = document.getElementById('file-preview');
const sendBtn = document.getElementById('send-btn');
const clearBtn = document.getElementById('clear-btn');
const presetSelector = document.getElementById('preset-selector');
const addPresetBtn = document.getElementById('add-preset-btn');
const deletePresetBtn = document.getElementById('delete-preset-btn');
let history = [];
let currentUserMessage = [];
let sessionId = localStorage.getItem('chatSessionId');
if (!sessionId) {
// 如果没有session_id,调用初始化接口
fetch('/init_session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
sessionId = data.session_id;
localStorage.setItem('chatSessionId', sessionId);
});
}
// Load presets
async function loadPresets() {
const response = await fetch('/presets');
const customPresets = await response.json();
customPresets.forEach(preset => {
const option = document.createElement('option');
option.value = preset.id;
option.textContent = preset.name;
presetSelector.appendChild(option);
});
}
// Add custom preset
addPresetBtn.addEventListener('click', async () => {
const presetName = prompt('输入预设名字:');
const presetContent = prompt('输入预设内容:');
if (presetName && presetContent) {
const response = await fetch('/add_preset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: presetName, content: presetContent })
});
const result = await response.json();
if (result.status === 'success') {
const option = document.createElement('option');
option.value = result.id;
option.textContent = presetName;
presetSelector.appendChild(option);
}
}
});
deletePresetBtn.addEventListener('click', async () => {
const selectedPreset = presetSelector.value;
if (selectedPreset && !defaultPresets.includes(selectedPreset)) {
const response = await fetch('/delete_preset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: selectedPreset })
});
const result = await response.json();
if (result.status === 'success') {
presetSelector.removeChild(presetSelector.querySelector(`option[value="${selectedPreset}"]`));
}
}
});
// 页面加载时获取历史记录
async function loadHistory() {
try {
const sessionId = localStorage.getItem('chatSessionId');
if (!sessionId) {
// 如果没有session_id,先初始化
const initResponse = await fetch('/init_session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
const initData = await initResponse.json();
localStorage.setItem('chatSessionId', initData.session_id);
}
// 获取历史记录时传递session_id
const response = await fetch(`/history?session_id=${localStorage.getItem('chatSessionId')}`);
const data = await response.json();
history = data;
// 显示历史消息
history.forEach(msg => {
addMessage(msg.role, msg.parts);
});
} catch (error) {
console.error('Failed to load chat history:', error);
}
}
// 建议在页面加载时就初始化session_id
document.addEventListener('DOMContentLoaded', async () => {
if (!localStorage.getItem('chatSessionId')) {
const response = await fetch('/init_session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
const data = await response.json();
localStorage.setItem('chatSessionId', data.session_id);
}
await loadHistory();
loadPresets();
});
function handleFileUpload(event) {
const files = event.target.files;
const filePreview = document.getElementById('file-preview');
Array.from(files).forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.file = file; // 存储文件引用
// 创建状态指示器容器
const statusOverlay = document.createElement('div');
statusOverlay.className = 'upload-status';
fileItem.appendChild(statusOverlay);
const deleteButton = document.createElement('button');
deleteButton.className = 'delete-button';
deleteButton.innerHTML = '×';
deleteButton.onclick = () => {
filePreview.removeChild(fileItem);
};
const fileInfo = document.createElement('div');
fileInfo.className = 'file-info';
fileInfo.innerHTML = `
<div>文件名: ${file.name}</div>
<div>类型: ${file.type || '未知'}</div>
<div>大小: ${(file.size / 1024).toFixed(2)} KB</div>
${file.path ? `<div>路径: ${file.path}</div>` : ''}
`;
if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.className = 'preview-image';
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
reader.readAsDataURL(file);
fileItem.appendChild(img);
} else {
fileItem.style.display = 'flex';
fileItem.style.alignItems = 'center';
fileItem.style.justifyContent = 'center';
fileItem.innerHTML += '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>';
}
fileItem.appendChild(deleteButton);
fileItem.appendChild(fileInfo);
filePreview.appendChild(fileItem);
});
}
async function uploadFile(fileItem) {
const file = fileItem.file;
const statusOverlay = fileItem.querySelector('.upload-status');
const spinner = document.createElement('div');
spinner.className = 'spinner';
// 显示加载状态
statusOverlay.innerHTML = '';
statusOverlay.appendChild(spinner);
statusOverlay.classList.add('active');
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
// 显示成功图标
statusOverlay.innerHTML = '<span class="success-icon">✓</span>';
currentUserMessage.push({
uri: result.gemini_uri,
name: result.filename,
localPath: file.path // 如果可用
});
console.log('File uploaded successfully:', result.gemini_uri, "\n", result.filename);
// 2秒后隐藏状态指示器
setTimeout(() => {
statusOverlay.classList.remove('active');
}, 2000);
return true;
}
throw new Error('Upload failed');
} catch (error) {
// 显示错误状态
statusOverlay.innerHTML = `
<span class="error-icon">×</span>
<div class="upload-error-message">上传失败</div>
`;
console.error('Upload failed:', error);
return false;
}
}
function addMessage(role, parts) {
const messageDiv = document.createElement('div');
messageDiv.className = `mb-4 ${role === 'user' ? 'text-right' : 'text-left'} w-full`;
const messageContent = `
<div class="inline-block break-words">
<span class="prose inline-block w-full p-3 rounded-lg ${role === 'user'
? 'bg-gradient-to-b from-blue-400 via-blue-500 to-blue-600 text-white rounded-br-none'
: 'bg-gradient-to-b from-yellow-100 via-yellow-200 to-yellow-300 text-gray-800 rounded-bl-none'
}">${role === 'user' ? parts[parts.length - 1] : MarkdownRenderer.render(parts[parts.length - 1])}</span>
</div>
`;
messageDiv.innerHTML = messageContent;
chatHistory.appendChild(messageDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
}
function adjustTextareaHeight() {
userInput.style.height = 'auto';
userInput.style.height = (userInput.scrollHeight) + 'px';
}
// Event listener for input changes
userInput.addEventListener('input', adjustTextareaHeight);
// Event listener for keydown events
userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Modify sendMessage function
async function sendMessage() {
const TYPING_CONFIG = {
SPEED: 10,
CHUNK_SIZE: 4,
SCROLL_BEHAVIOR: 'smooth'
};
// 获取所有预览文件
const filePreview = document.getElementById('file-preview');
const fileItems = filePreview.querySelectorAll('.file-item');
// 上传所有文件
const uploadPromises = Array.from(fileItems).map(fileItem => uploadFile(fileItem));
await Promise.all(uploadPromises);
// 清空文件预览区
filePreview.innerHTML = '';
userTextMessage = userInput.value.trim();
if (!userTextMessage) userTextMessage = ' ';
// 原有的消息发送逻辑
currentUserMessage.push(userTextMessage);
const Message = { role: 'user', parts: currentUserMessage }
if (!currentUserMessage.length) return;
history.push(Message);
console.log('history:', history);
addMessage('user', currentUserMessage);
userInput.value = '';
currentUserMessage = [];
adjustTextareaHeight();
const selectedPreset = presetSelector.value;
try {
const response = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userMessage: Message,
preset: selectedPreset,
session_id: localStorage.getItem('chatSessionId')
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let aiResponse = '';
let displayedResponse = '';
let aiMessageDiv = null;
const typeWriter = (text, index, callback) => {
if (index < text.length) {
displayedResponse = text.substring(0, index + TYPING_CONFIG.CHUNK_SIZE);
if (aiMessageDiv) {
aiMessageDiv.innerHTML = `<span class="prose inline-block p-2 rounded-lg bg-gradient-to-b from-green-200 via-green-250 to-green-300">
${MarkdownRenderer.render(displayedResponse)}</span>`;
chatHistory.scrollTop = chatHistory.scrollHeight;
chatHistory.style.scrollBehavior = TYPING_CONFIG.SCROLL_BEHAVIOR;
}
setTimeout(() => typeWriter(text, index + TYPING_CONFIG.CHUNK_SIZE, callback), TYPING_CONFIG.SPEED);
} else {
callback();
}
};
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
aiResponse += chunk;
if (!aiMessageDiv) {
aiMessageDiv = document.createElement('div');
aiMessageDiv.className = 'mb-2 text-left';
chatHistory.appendChild(aiMessageDiv);
}
await new Promise(resolve => {
typeWriter(aiResponse, displayedResponse.length, resolve);
});
}
history.push({ role: 'model', part: [aiResponse] });
} catch (error) {
console.error('Error sending message:', error);
addMessage('model', 'Sorry, something went wrong. Please try again.');
}
}
// 清除聊天记录
async function clearChat() {
try {
const response = await fetch(`/clear_history?session_id=${localStorage.getItem('chatSessionId')}`, {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
chatHistory.innerHTML = '';
history = [];
} else {
console.error('Failed to clear chat history:', result.message);
}
} catch (error) {
console.error('Error clearing chat:', error);
}
}
sendBtn.addEventListener('click', sendMessage);
clearBtn.addEventListener('click', clearChat);
// 防止输入框在移动设备上弹出键盘时影响布局
const viewport = document.querySelector('meta[name=viewport]');
viewport.setAttribute('content', viewport.content + ', height=' + window.innerHeight);
</script>
</body>
</html>