Spaces:
Sleeping
Sleeping
<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> |