# -*- coding: UTF-8 -*-
import logging
from datetime import datetime
from collections import defaultdict
from typing import List, Dict
import requests
import json
from flask import Flask, Response, request, jsonify, render_template_string
from flask_cors import CORS
import uuid
import re
from bs4 import BeautifulSoup
import time
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 初始化 Flask 应用
app = Flask(__name__)
CORS(app)
sessions = defaultdict(list)
# Gemini API 配置
GOOGLE_API_KEY = "你的apikey"
GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-thinking-exp-01-21:streamGenerateContent"
def fetch_url_content(url: str) -> str:
"""获取网页文本内容"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
try:
response = requests.get(url, headers=headers, timeout=10, verify=True)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
return soup.get_text(separator=' ', strip=True)[:65536]
except requests.exceptions.RequestException as e:
return f"链接获取失败: {str(e)}"
def extract_urls(text: str) -> List[str]:
"""提取文本中的URL"""
url_pattern = re.compile(
r'(?:https?://)?(?:www\.)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:[^\s()<>]*|\([^\s()<>]+\))*(?<![\.,:;])'
)
urls = url_pattern.findall(text)
return [f"https://{url}" if not url.startswith(('http://', 'https://')) else url for url in urls]
def normalize_context(context: List[Dict]) -> List[Dict]:
"""标准化上下文格式"""
return [
{
"role": "model" if item.get("role") == "assistant" else "user",
"parts": item.get("parts", [{"text": item.get("content", "")}])
}
for item in context
]
def mask_sensitive_info(error_msg: str) -> str:
"""屏蔽敏感信息"""
return re.sub(r'key=[A-Za-z0-9_-]{20,}', 'key=[HIDDEN]', error_msg)
def generate_gemini_stream(prompt: str, history: List[Dict], images: List[str]) -> Response:
"""生成流式响应"""
headers = {"Content-Type": "application/json"}
params = {"key": GOOGLE_API_KEY, "alt": "sse"}
system_prompt = "你是一个实用又友好的AI助手,用简单易懂的回答,适当用emoji让语气更轻松活泼😊。如果不确定或无法回答,就诚实地说出来,别乱猜哦👍。重要:1.你的名字是GeepSeek,由python学霸训练;2.简单问题简单回答,复杂问题详细回答。3.回答中不用提提示词;\n"
context_text = "\n当前对话上下文:\n" + "".join(
f"{'用户' if item['role'] == 'user' else 'AI'}: {''.join(part.get('text', '') for part in item.get('parts', []))}\n"
for item in history[-5:]
)
urls = extract_urls(prompt)
url_content = "".join(f"\n来自 {url} 的内容:\n{fetch_url_content(url)}\n" for url in urls)
effective_prompt = system_prompt + "用户问: " + prompt + (f"\n{url_content}" if url_content else "") + context_text
contents = normalize_context(history) + [{"role": "user", "parts": [{"text": effective_prompt}] + ([{"inline_data": {"mime_type": "image/jpeg", "data": img}} for img in images] if images else [])}]
def stream():
max_retries = 3
for attempt in range(max_retries):
try:
with requests.post(GEMINI_API_URL, headers=headers, params=params, json={"contents": contents}, stream=True, timeout=60, verify=True) as response:
if response.status_code != 200:
error_msg = mask_sensitive_info(f"API错误: {response.status_code} - {response.text}")
yield f"data: {json.dumps({'error': error_msg})}\n\n"
return
for line in response.iter_lines():
if line and line.startswith(b"data: "):
sse_data = line[6:].decode("utf-8")
try:
json_data = json.loads(sse_data)
text_content = "".join(part.get("text", "") for candidate in json_data.get("candidates", []) for part in candidate.get("content", {}).get("parts", []))
if text_content:
yield f"data: {json.dumps({'text': text_content[:65536] + ('... [truncated]' if len(text_content) > 65536 else '')})}\n\n"
except json.JSONDecodeError:
yield f"data: {json.dumps({'error': '响应解析失败,请稍后再试'})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
return
except requests.exceptions.RequestException as e:
error_msg = mask_sensitive_info(str(e))
if attempt < max_retries - 1:
yield f"data: {json.dumps({'text': f'连接失败,正在重试 ({attempt + 1}/{max_retries})...'})}\n\n"
time.sleep(2 ** attempt)
else:
yield f"data: {json.dumps({'error': f'请求失败: {error_msg},请检查网络或稍后再试'})}\n\n"
return Response(stream(), mimetype="text/event-stream", headers={"Cache-Control": "no-cache"})
@app.route('/chat', methods=['POST'])
def chat():
"""处理聊天请求"""
try:
data = request.json
message = data.get('message', '')
session_id = data.get('session_id', str(uuid.uuid4()))
context = data.get('context', [])
images = data.get('images', [])
sessions[session_id].append({'role': 'user', 'parts': [{"text": message}], 'timestamp': datetime.now().isoformat(), 'images': images})
return generate_gemini_stream(message, context, images)
except Exception as e:
logger.error(f"请求错误: {str(e)}")
return Response(
f"data: {json.dumps({'error': '服务器错误,请稍后再试'})}",
mimetype="text/event-stream",
headers={"Cache-Control": "no-cache"}
)
@app.route('/')
def index():
"""渲染首页"""
try:
return render_template_string(r'''<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>GeepSeek-智能AI</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
:root {
--primary: #000000;
--secondary: #666666;
--bg: #ffffff;
--text: #000000;
--border: #cccccc;
--shadow: rgba(0, 0, 0, 0.1);
--hover: #f0f0f0;
--code-bg: #1e1e1e;
--code-text: #d4d4d4;
--error: #ff4444;
--button-bg: #e0e0e0;
--button-hover: #cccccc;
--ai-bg: #e0e0e0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
font-size: 18px;
min-height: 100vh;
overflow-x: hidden;
word-break: break-word;
overflow-wrap: break-word;
display: flex;
justify-content: center;
align-items: center;
}
.container {
display: flex;
height: 100vh;
width: 100%;
max-width: 1400px;
border-radius: 12px;
overflow: hidden;
background: var(--bg);
box-shadow: 0 4px 12px var(--shadow);
position: relative;
}
.sidebar {
width: 25%;
max-width: 350px;
background: #f1f1f1;
border-right: 1px solid var(--border);
height: 100vh;
overflow-y: auto;
position: fixed;
left: -100%;
transition: left 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
.sidebar.active { left: 0; }
.menu-toggle {
position: fixed;
top: 10px;
left: 10px;
background: none;
border: none;
cursor: pointer;
z-index: 1001;
padding: 8px;
}
.menu-toggle.hidden { opacity: 0; pointer-events: none; }
.new-chat-btn {
position: fixed;
top: 10px;
right: 10px;
padding: 8px;
background: var(--primary);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background 0.3s;
box-shadow: 0 3px 8px var(--shadow);
width: 40px;
height: 40px;
z-index: 1001;
}
.new-chat-btn:hover { background: #333333; }
.history-search {
margin: 8% 8% 5%;
padding: 12px;
border: 1px solid var(--border);
border-radius: 8px;
width: 85%;
font-size: 16px;
background: #fff;
box-shadow: inset 0 2px 4px var(--shadow);
}
.chat-history {
flex: 1;
overflow-y: auto;
padding: 0 5%;
scrollbar-width: thin;
scrollbar-color: var(--secondary) var(--bg);
}
.chat-history::-webkit-scrollbar { width: 8px; }
.chat-history::-webkit-scrollbar-thumb { background: var(--secondary); border-radius: 4px; }
.history-item {
padding: 12px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.2s;
border-radius: 8px;
margin: 5px 0;
font-size: 16px;
}
.history-item:hover { background: var(--hover); }
.history-item.active { background: #e0e0e0; }
.history-item .title { font-weight: 600; font-size: 18px; color: var(--text); }
.history-item .preview { font-size: 14px; color: var(--secondary); margin-top: 5px; }
.history-item .time { font-size: 12px; color: #999; margin-top: 5px; display: block; }
.clear-history-container {
position: sticky;
bottom: 0;
padding: 10px 8%;
background: #f1f1f1;
border-top: 1px solid var(--border);
}
.clear-history {
padding: 12px 20px;
background: var(--secondary);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: background 0.3s;
box-shadow: 0 3px 8px var(--shadow);
width: 100%;
font-size: 18px;
display: flex;
justify-content: center;
}
.clear-history:hover { background: #4d4d4d; }
.chat-container {
flex: 3;
display: flex;
flex-direction: column;
padding: 20px;
background: var(--bg);
border-radius: 0 12px 12px 0;
width: 100%;
height: 100vh;
position: relative;
padding-bottom: 1px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 0 10px;
scrollbar-width: thin;
scrollbar-color: var(--secondary) var(--bg);
display: flex;
flex-direction: column;
padding-bottom: 2px;
line-height: 1.8;
margin-top: 40px;
}
.chat-messages::-webkit-scrollbar { width: 8px; }
.chat-messages::-webkit-scrollbar-thumb { background: var(--secondary); border-radius: 4px; }
.message {
display: flex;
margin: 12px 0;
padding: 12px 18px;
max-width: 80%;
border-radius: 12px;
animation: fadeIn 0.3s ease;
box-shadow: 0 2px 6px var(--shadow);
font-size: 18px;
overflow-wrap: break-word;
word-break: break-word;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
.message.user {
margin-left: auto;
background: var(--primary);
color: #fff;
border-bottom-right-radius: 4px;
}
.message.ai {
margin-right: auto;
background: var(--ai-bg);
color: var(--text);
border-bottom-left-radius: 4px;
padding-left: 10px;
position: relative;
}
.message.ai::before {
content: '';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
border-radius: 50%;
background-size: contain;
}
.message-content {
line-height: 1.6;
overflow-wrap: break-word;
word-break: break-word;
padding-left: 10px;
width: 100%;
position: relative;
}
.message-content p { margin: 10px 0; }
.message-content a { color: blue; text-decoration: underline; }
.message-content a:hover { color: #333333; }
.message-content ul, .message-content ol { width: 99%; }
.message-content li { margin: 5px 0; }
.message-content blockquote {
margin: 10px 0;
padding: 10px 15px;
background: #f0f0f0;
border-left: 4px solid var(--primary);
color: #333333;
font-style: italic;
}
.message-content pre {
background: var(--code-bg);
color: var(--code-text);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
position: relative;
margin: 10px 0;
font-size: 16px;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
}
.message-content code {
background: var(--code-bg);
color: var(--code-text);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
font-size: 16px;
}
.message-content pre code {
background: none;
color: var(--code-text);
padding: 0;
}
.code-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 6px;
z-index: 10;
}
.code-actions button {
background: var(--button-bg);
color: #333;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px var(--shadow);
touch-action: manipulation;
}
.code-actions button:hover {
background: var(--button-hover);
}
.code-actions button:active {
background: #b0b0b0;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.loading-dots {
display: flex;
gap: 8px;
}
.loading-dots span {
width: 12px;
height: 12px;
background: var(--primary);
border-radius: 50%;
animation: bounce 1.2s infinite ease-in-out both;
}
.loading-dots span:nth-child(1) { animation-delay: -0.3s; }
.loading-dots span:nth-child(2) { animation-delay: -0.15s; }
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
.input-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 15px;
border-top: 1px solid var(--border);
background: var(--bg);
z-index: 100;
width: 100%;
max-width: 1400px;
margin: 0 auto;
}
.input-wrapper {
display: flex;
gap: 12px;
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
align-items: center;
box-shadow: 0 2px 5px var(--shadow);
width: 100%;
}
#message-input {
flex: 1;
border: none;
outline: none;
padding: 5px;
resize: none;
max-height: 20vh;
font-size: 18px;
background: transparent;
color: var(--text);
overflow-y: auto;
transition: all 0.2s ease;
}
#send-button, #stop-button, .upload-button {
background: none;
border: none;
cursor: pointer;
padding: 8px;
color: var(--primary);
transition: color 0.3s;
}
#send-button:hover, #stop-button:hover, .upload-button:hover { color: #333333; }
#stop-button { display: none; }
.image-preview-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
width: 100%;
}
.image-preview-container:empty { display: none; }
.image-preview-item {
max-width: 100px;
max-height: 100px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px var(--shadow);
position: relative;
}
.image-preview-item img { width: 100%; height: 100%; object-fit: cover; }
.remove-image {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
font-size: 12px;
line-height: 20px;
text-align: center;
}
.error-message {
color: var(--error);
font-size: 16px;
margin: 10px 0;
text-align: center;
}
@media (max-width: 600px) {
.container { flex-direction: column; height: auto; padding-bottom: 2px; }
.sidebar { width: 80%; max-width: none; height: 100vh; }
.chat-container { flex: none; margin: 0; padding: 15px; padding-bottom: 2px; }
.input-container { padding: 10px; }
.input-wrapper { padding: 10px; }
.message { max-width: 90%; font-size: 16px; }
.menu-toggle { top: 5px; left: 5px; }
.new-chat-btn { top: 5px; right: 5px; width: 36px; height: 36px; }
.message-content pre { font-size: 14px; padding: 12px; max-height: 300px; }
.code-actions { top: 6px; right: 6px; }
.code-actions button { padding: 5px 10px; font-size: 12px; }
#message-input { font-size: 16px; }
.clear-history { font-size: 16px; padding: 10px 15px; }
}
</style>
</head>
<body>
<button class="menu-toggle">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M3 12h18M3 6h18M3 18h18" stroke="var(--primary)" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<button class="new-chat-btn">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 3.33337V12.6667M3.33333 8H12.6667" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<div class="container">
<div class="sidebar">
<input type="text" class="history-search" placeholder="搜索历史记录...">
<div class="chat-history"></div>
<div class="clear-history-container">
<button class="clear-history">清空历史</button>
</div>
</div>
<div class="chat-container">
<div class="chat-messages"></div>
<div class="input-container">
<div class="image-preview-container" id="image-preview"></div>
<div class="input-wrapper">
<button class="upload-button" id="upload-button">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M14 10V12H2V10H0V14H16V10H14ZM8 2L12 6H9V10H7V6H4L8 2Z" fill="var(--primary)"/>
</svg>
</button>
<textarea id="message-input" placeholder="输入消息或Ctrl+V粘贴图片..." rows="1"></textarea>
<button id="send-button">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M14.6667 1.33337L7.33333 8.66671M14.6667 1.33337L10 14.6667L7.33333 8.66671L1.33333 6.00004L14.6667 1.33337Z" stroke="var(--primary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button id="stop-button">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M4 4H12V12H4V4Z" stroke="var(--primary)" stroke-width="2" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
</div>
<div id="html-preview-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); z-index: 1001; overflow: auto;">
<div style="background: #fff; margin: 10% auto; padding: 20px; border-radius: 12px; width: 90%; max-width: 800px; position: relative; box-shadow: 0 5px 20px var(--shadow);">
<button onclick="document.getElementById('html-preview-modal').style.display='none'" style="position: absolute; top: 10px; right: 10px; background: none; border: none; cursor: pointer; font-size: 1.5rem; color: var(--text);">×</button>
<iframe id="html-preview-content" style="width: 100%; height: 500px; border: none;"></iframe>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
marked.setOptions({
gfm: true,
breaks: true,
smartLists: true,
async: true,
ignoreUnescapedHTML: true,
highlight: (code, lang) => {
if (lang && hljs.getLanguage(lang) && !isNaturalText(code)) {
return hljs.highlight(code, { language: lang }).value;
}
return code;
}
});
initializeChat();
});
function isNaturalText(text) {
const naturalTextPatterns = [
/[.!?]$/,
/\s+[a-zA-Z]{2,}\s+[a-zA-Z]{2,}/,
/[^\w\s{}/\\<>;:]+/
];
return naturalTextPatterns.some(pattern => pattern.test(text)) &&
!text.match(/^[\s]*(function|class|if|for|while|const|let|var)/);
}
const Config = {
MAX_CONTEXT_LENGTH: 10,
MAX_MESSAGE_LENGTH: 65536,
MAX_IMAGE_UPLOADS: 5,
CHUNK_SIZE: 4096
};
let conversations = JSON.parse(localStorage.getItem('conversations')) || [];
let currentConversationId = localStorage.getItem('currentConversationId') || Date.now().toString();
let currentContext = [];
let eventSource = null;
let uploadedImages = [];
let isGenerating = false;
const chatMessages = document.querySelector('.chat-messages');
const messageInput = document.querySelector('#message-input');
const sendButton = document.querySelector('#send-button');
const stopButton = document.querySelector('#stop-button');
const uploadButton = document.querySelector('#upload-button');
const imagePreview = document.querySelector('#image-preview');
const sidebar = document.querySelector('.sidebar');
const menuToggle = document.querySelector('.menu-toggle');
const newChatBtn = document.querySelector('.new-chat-btn');
const clearHistoryBtn = document.querySelector('.clear-history');
const historySearch = document.querySelector('.history-search');
const htmlPreviewModal = document.querySelector('#html-preview-modal');
const htmlPreviewContent = document.querySelector('#html-preview-content');
function initializeChat() {
conversations.forEach(conv => {
conv.messages.forEach(msg => {
if (msg.content && !msg.parts) msg.parts = [{"text": msg.content}], delete msg.content;
if (msg.role === "assistant") msg.role = "model";
if (msg.role === "system") msg.role = "user";
});
});
loadConversation(currentConversationId);
updateChatHistory();
setupEventListeners();
adjustInputHeight();
adjustContainerHeight();
}
function setupEventListeners() {
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
menuToggle.classList.toggle('hidden');
document.body.style.overflow = sidebar.classList.contains('active') ? 'hidden' : 'auto';
});
document.addEventListener('click', e => {
if (!sidebar.contains(e.target) && !menuToggle.contains(e.target) && sidebar.classList.contains('active')) {
sidebar.classList.remove('active');
menuToggle.classList.remove('hidden');
document.body.style.overflow = 'auto';
}
});
sendButton.addEventListener('click', debounce(sendMessage, 300));
stopButton.addEventListener('click', stopGenerating);
messageInput.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
debounce(sendMessage, 300)();
}
});
messageInput.addEventListener('input', adjustInputHeight);
messageInput.addEventListener('paste', handlePaste);
uploadButton.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.multiple = true;
input.onchange = handleImageUpload;
input.click();
});
newChatBtn.addEventListener('click', startNewChat);
clearHistoryBtn.addEventListener('click', clearHistory);
historySearch.addEventListener('input', e => updateChatHistory(e.target.value.toLowerCase()));
window.addEventListener('resize', adjustContainerHeight);
}
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function adjustInputHeight() {
messageInput.style.height = 'auto';
messageInput.style.height = `${Math.min(messageInput.scrollHeight, window.innerHeight * 0.2)}px`;
}
function adjustContainerHeight() {
const chatContainer = document.querySelector('.chat-container');
const inputContainer = document.querySelector('.input-container');
chatContainer.style.paddingBottom = `${inputContainer.offsetHeight + 10}px`;
}
function handleImageUpload(e) {
const remainingSlots = Config.MAX_IMAGE_UPLOADS - uploadedImages.length;
if (remainingSlots <= 0) {
alert('最多只能上传5张图片!');
return;
}
const filesToProcess = Array.from(e.target.files).slice(0, remainingSlots);
filesToProcess.forEach(file => {
const reader = new FileReader();
reader.onload = event => addImageToPreview(event.target.result.split(',')[1]);
reader.readAsDataURL(file);
});
}
function handlePaste(e) {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const remainingSlots = Config.MAX_IMAGE_UPLOADS - uploadedImages.length;
if (remainingSlots <= 0) return;
let imageCount = 0;
for (const item of items) {
if (item.type.indexOf('image') !== -1 && imageCount < remainingSlots) {
const blob = item.getAsFile();
const reader = new FileReader();
reader.onload = event => addImageToPreview(event.target.result.split(',')[1]);
reader.readAsDataURL(blob);
imageCount++;
}
}
}
function addImageToPreview(base64Data) {
if (uploadedImages.length >= Config.MAX_IMAGE_UPLOADS) return;
const imgDiv = document.createElement('div');
imgDiv.className = 'image-preview-item';
imgDiv.innerHTML = `<img src="data:image/jpeg;base64,${base64Data}" alt="Uploaded Image">
<button class="remove-image" onclick="this.parentElement.remove(); uploadedImages = uploadedImages.filter(img => img !== '${base64Data}');">×</button>`;
imagePreview.appendChild(imgDiv);
uploadedImages.push(base64Data);
}
function updateChatHistory(searchTerm = '') {
const chatHistory = document.querySelector('.chat-history');
chatHistory.innerHTML = '';
conversations
.sort((a, b) => new Date(b.messages?.slice(-1)[0]?.timestamp || 0) - new Date(a.messages?.slice(-1)[0]?.timestamp || 0))
.filter(c => !searchTerm || c.messages.some(m => m.parts?.[0]?.text.toLowerCase().includes(searchTerm)))
.forEach(c => {
if (!c.messages?.length) return;
const firstUserMessage = c.messages.find(m => m.role === 'user');
const titleText = firstUserMessage?.parts?.[0]?.text.slice(0, 30) + (firstUserMessage?.parts?.[0]?.text.length > 30 ? '...' : '') || '新对话';
const div = document.createElement('div');
div.className = `history-item ${c.id === currentConversationId ? 'active' : ''}`;
div.innerHTML = `
<div class="title">${titleText}</div>
<div class="preview">${c.messages.slice(-2).map(m => `<div>${m.parts?.[0]?.text.slice(0, 50)}${m.parts?.[0]?.text.length > 50 ? '...' : ''}</div>`).join('')}</div>
<div class="time">${new Date(c.messages.slice(-1)[0].timestamp).toLocaleString()}</div>
`;
div.onclick = () => loadConversation(c.id);
chatHistory.appendChild(div);
});
}
function loadConversation(id) {
currentConversationId = id;
localStorage.setItem('currentConversationId', id);
const conversation = conversations.find(c => c.id === id);
if (!conversation) return;
chatMessages.innerHTML = '';
conversation.messages.forEach(msg => {
const images = msg.parts.filter(part => part.inline_data).map(part => part.inline_data.data);
addMessage(msg.parts.find(part => part.text)?.text || '', msg.role === 'user' ? 'user' : 'ai', images);
});
currentContext = conversation.messages.map(msg => ({
role: msg.role,
parts: msg.parts.map(part => part.inline_data ? {inline_data: part.inline_data} : {text: part.text})
}));
updateChatHistory();
sidebar.classList.remove('active');
menuToggle.classList.remove('hidden');
document.body.style.overflow = 'auto';
}
async function addMessage(text, type = 'user', images = null) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
if (text.length > Config.CHUNK_SIZE) {
contentDiv.innerHTML = '<span>正在加载...</span>';
for (let i = 0; i < text.length; i += Config.CHUNK_SIZE) {
await new Promise(resolve => setTimeout(resolve, 50));
contentDiv.innerHTML = await marked.parse(text.slice(0, i + Config.CHUNK_SIZE));
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
}
} else {
contentDiv.innerHTML = await marked.parse(text);
}
if (type === 'ai') addCodeActions(contentDiv);
if (images?.length) {
const imgContainer = document.createElement('div');
imgContainer.className = 'image-preview-container';
images.forEach(imgData => {
const img = document.createElement('div');
img.className = 'image-preview-item';
img.innerHTML = `<br><img src="data:image/jpeg;base64,${imgData}" alt="Image">`;
imgContainer.appendChild(img);
});
messageDiv.appendChild(imgContainer);
}
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
}
function addCodeActions(contentDiv) {
contentDiv.querySelectorAll('pre code').forEach(block => {
if (block.parentNode.querySelector('.code-actions')) return;
const pre = block.parentNode;
const actions = document.createElement('div');
actions.className = 'code-actions';
actions.innerHTML = `
<button onclick="copyCode(this)">复制</button>
${block.textContent.trim().toLowerCase().startsWith('<!doctype html') || block.textContent.trim().startsWith('<html')
? '<button onclick="previewHTML(this)">预览</button>'
: ''}
`;
pre.style.position = 'relative';
pre.appendChild(actions);
hljs.highlightElement(block);
});
}
function copyCode(button) {
const code = button.closest('pre').querySelector('code').textContent;
const copyText = (text) => {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
return Promise.resolve();
}
};
copyText(code).then(() => {
button.textContent = '已复制';
setTimeout(() => button.textContent = '复制', 2000);
}).catch(err => {
console.error('复制失败:', err);
button.textContent = '复制失败';
setTimeout(() => button.textContent = '复制', 2000);
});
}
function previewHTML(button) {
const code = button.closest('pre').querySelector('code').textContent;
htmlPreviewContent.srcdoc = code;
htmlPreviewModal.style.display = 'block';
}
async function sendMessage() {
const message = messageInput.value.trim();
if (!message && !uploadedImages.length || isGenerating) return;
startGeneratingState();
if (eventSource) eventSource.close();
const imagesToSend = [...uploadedImages];
messageInput.value = '';
imagePreview.innerHTML = '';
uploadedImages = [];
adjustInputHeight();
// 保存用户消息到历史记录
const userParts = [{"text": message || "请描述这些图片"}].concat(
imagesToSend.map(img => ({"inline_data": {"mime_type": "image/jpeg", "data": img}}))
);
currentContext.push({ role: 'user', parts: userParts });
await addMessage(message || '[图片消息]', 'user', imagesToSend);
const loadingDiv = addLoadingMessage();
try {
const response = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message || "请描述这些图片",
session_id: currentConversationId,
context: currentContext,
images: imagesToSend
})
});
if (!response.ok) {
throw new Error(`服务器错误: ${response.status}`);
}
let accumulatedText = '';
const aiMessageDiv = document.createElement('div');
aiMessageDiv.className = 'message ai';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
aiMessageDiv.appendChild(contentDiv);
chatMessages.replaceChild(aiMessageDiv, loadingDiv);
const reader = response.body.getReader();
const decoder = new TextDecoder();
eventSource = { close: () => { reader.cancel(); endGeneratingState(); } };
async function processStream() {
try {
const { done, value } = await reader.read();
if (done) {
currentContext.push({ role: 'model', parts: [{"text": accumulatedText}] });
currentContext = currentContext.slice(-Config.MAX_CONTEXT_LENGTH);
saveToHistory(message || "请描述这些图片", accumulatedText, imagesToSend);
endGeneratingState();
eventSource = null;
return;
}
const chunk = decoder.decode(value);
chunk.split('\n\n').forEach(async line => {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.text) {
accumulatedText += data.text;
contentDiv.innerHTML = await marked.parse(accumulatedText);
addCodeActions(contentDiv);
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
} else if (data.error) {
contentDiv.innerHTML = `<span class="error-message">抱歉,出了点问题:${escape(data.error)}</span>`;
endGeneratingState();
eventSource.close();
eventSource = null;
}
} catch (e) {
console.error("Stream parse error:", e);
contentDiv.innerHTML += '<span class="error-message">解析失败,请稍后再试</span>';
}
}
});
if (isGenerating) processStream();
else {
reader.cancel();
endGeneratingState();
eventSource = null;
}
} catch (e) {
console.error("Stream read error:", e);
chatMessages.removeChild(aiMessageDiv);
addMessage(`网络错误:${e.message},请检查网络后重试哦😅`, 'ai');
saveToHistory(message || "请描述这些图片", accumulatedText || '[网络中断]', imagesToSend);
endGeneratingState();
eventSource = null;
}
}
processStream();
} catch (error) {
console.error("Fetch error:", error);
chatMessages.removeChild(loadingDiv);
addMessage(`哎呀,出了点问题:${error.message},请稍后再试哦😅`, 'ai');
saveToHistory(message || "请描述这些图片", '[请求失败]', imagesToSend);
endGeneratingState();
eventSource = null;
}
}
function stopGenerating() {
if (eventSource && isGenerating) {
isGenerating = false;
stopButton.style.display = 'none';
sendButton.style.display = 'inline-block';
eventSource.close();
}
}
function startGeneratingState() {
isGenerating = true;
sendButton.style.display = 'none';
stopButton.style.display = 'inline-block';
}
function endGeneratingState() {
isGenerating = false;
sendButton.style.display = 'inline-block';
stopButton.style.display = 'none';
}
function addLoadingMessage() {
const div = document.createElement('div');
div.className = 'message ai';
div.innerHTML = `
<div class="message-content">
<div class="loading-container">
<div class="loading-dots"><span></span><span></span><span></span></div>
</div>
</div>`;
chatMessages.appendChild(div);
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
return div;
}
function saveToHistory(message, response, images) {
let conversation = conversations.find(c => c.id === currentConversationId);
if (!conversation) {
conversation = { id: currentConversationId, messages: [] };
conversations.push(conversation);
}
const userParts = [{"text": message}].concat(
images.map(img => ({
"inline_data": {
"mime_type": "image/jpeg",
"data": img
}
}))
);
conversation.messages.push(
{ role: 'user', parts: userParts, timestamp: new Date().toISOString() },
{ role: 'model', parts: [{"text": response}], timestamp: new Date().toISOString() }
);
localStorage.setItem('conversations', JSON.stringify(conversations));
updateChatHistory();
}
function startNewChat() {
currentConversationId = Date.now().toString();
localStorage.setItem('currentConversationId', currentConversationId);
chatMessages.innerHTML = '';
imagePreview.innerHTML = '';
uploadedImages = [];
currentContext = [];
conversations.push({ id: currentConversationId, messages: [] });
localStorage.setItem('conversations', JSON.stringify(conversations));
updateChatHistory();
sidebar.classList.remove('active');
menuToggle.classList.remove('hidden');
document.body.style.overflow = 'auto';
}
function clearHistory() {
if (confirm('确定要清空历史记录吗?')) {
conversations = [];
localStorage.setItem('conversations', JSON.stringify(conversations));
startNewChat();
}
}
</script>
</body>
</html>
''')
except Exception as e:
logger.error(f"渲染首页错误: {str(e)}")
return "模板加载失败", 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)