GeepSeek来临,吊打DeepSeek!
功能有:url解析,图片解析,智能聊天,历史记录等等。
·
geepseek官网:
https://geepseek.pythonanywhere.com/
功能有:url解析,图片解析,智能聊天,历史记录等等
源代码:
# -*- 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 warnings
import urllib3
from bs4 import BeautifulSoup
import re
# Disable SSL verification warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
warnings.filterwarnings("ignore", category=UserWarning, module='bs4')
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Initialize Flask app
app = Flask(__name__)
CORS(app)
sessions = defaultdict(list)
# Gemini API Configuration
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)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
all_text = soup.get_text(separator=' ', strip=True)
return all_text[:65536] # 限制为 65536 字符
except requests.exceptions.RequestException as e:
return f"哎呀,链接获取失败了😅 错误信息: {e}"
def extract_urls(text: str) -> List[str]:
"""优化链接提取算法,支持文字混合情况"""
url_pattern = re.compile(
r'(?:https?://)?' # 可选的协议头
r'(?:www\.)?' # 可选的 www
r'(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}' # 域名部分
r'(?:[^\s()<>]*|\([^\s()<>]+\))*' # 路径和参数部分
r'(?<![\.,:;])' # 避免匹配末尾的标点
)
urls = url_pattern.findall(text)
cleaned_urls = []
for url in urls:
if not url.startswith(('http://', 'https://')):
url = 'https://' + url # 默认添加 https
url = re.sub(r'[^\w/?&=#-]$', '', url) # 移除尾部无效字符
cleaned_urls.append(url)
return cleaned_urls
def normalize_context(context: List[Dict]) -> List[Dict]:
"""Normalize context for Gemini API"""
return [
{
"role": "model" if item.get("role") == "assistant" else "user",
"parts": item.get("parts", [{"text": item.get("content", "")}])
}
for item in context
]
def generate_gemini_stream(prompt: str, history: List[Dict], images: List[str]) -> Response:
"""Generate a streaming response from Gemini API with URL content and conversation context"""
headers = {"Content-Type": "application/json"}
params = {"key": GOOGLE_API_KEY, "alt": "sse"}
# 系统提示词
system_prompt = "你是一个实用又友好的AI助手,用简单易懂的语言回答问题,像朋友一样自然亲切。回答时尽量提供准确有用的信息,适当用emoji让语气更轻松活泼😊。如果用户提供链接,请结合链接内容和当前对话上下文回答,并简要总结网页要点。如果用户上传图片,请描述图片内容并结合问题和上下文回答。如果不确定或无法回答,就诚实地说出来,别乱猜哦👍。\n"
# 提取当前对话上下文
context_text = "\n当前对话上下文:\n"
for item in history[-5:]: # 取最近 5 条对话作为上下文
role = "用户" if item["role"] == "user" else "AI"
parts_text = "".join(part.get("text", "") for part in item.get("parts", []))
context_text += f"{role}: {parts_text}\n"
# 检查提示词中是否有URL并获取内容
urls = extract_urls(prompt)
url_content = ""
if urls:
for url in urls:
url_content += f"\n来自 {url} 的内容:\n{fetch_url_content(url)}\n"
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():
try:
with requests.post(GEMINI_API_URL, headers=headers, params=params, json={"contents": contents}, stream=True, timeout=60) as response:
if response.status_code != 200:
yield f"data: {json.dumps({'error': f'API error: {response.status_code} - {response.text}'})}\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': 'API response parse error'})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'error': f'Error: {str(e)}'})}\n\n"
return Response(stream(), mimetype="text/event-stream", headers={"Cache-Control": "no-cache"})
@app.route('/chat', methods=['POST'])
def chat():
"""Handle chat requests"""
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()})
return generate_gemini_stream(message, context, images)
except Exception as e:
logger.error(f"Request error: {str(e)}")
return jsonify({'status': 'error', 'message': 'Server error'}), 500
@app.route('/')
def index():
"""Render the homepage"""
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-dimmed.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/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; /* Black for accents */
--secondary: #666666; /* Grey for secondary elements */
--bg: #ffffff; /* White background */
--text: #000000; /* Black text */
--border: #cccccc; /* Light grey borders */
--shadow: rgba(0, 0, 0, 0.1); /* Subtle black shadow */
--hover: #f0f0f0; /* Light grey hover */
--code-bg: #1e1e1e; /* Dark VS Code-like background for code */
--code-text: #d4d4d4; /* Light grey text for code */
}
* { 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: #fff;
box-shadow: 0 4px 12px var(--shadow);
}
.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 {
margin: 8% 8% 5%;
padding: 12px 20px;
background: var(--primary);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.3s;
box-shadow: 0 3px 8px var(--shadow);
width: 85%;
font-size: 18px;
justify-content: center;
}
.new-chat-btn:hover { background: #333333; }
.history-search {
margin: 0 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: thick;
scrollbar-color: var(--secondary) var(--bg);
}
.chat-history::-webkit-scrollbar { width: 12px; }
.chat-history::-webkit-scrollbar-thumb { background: var(--secondary); border-radius: 6px; border: 2px solid var(--bg); }
.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: #fff;
border-radius: 0 12px 12px 0;
width: 100%;
height: 100vh;
position: relative;
padding-bottom: 100px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 0 10px;
scrollbar-width: thick;
scrollbar-color: var(--secondary) var(--bg);
display: flex;
flex-direction: column;
padding-bottom: 20px;
line-height: 1.8;
}
.chat-messages::-webkit-scrollbar { width: 12px; }
.chat-messages::-webkit-scrollbar-thumb { background: var(--secondary); border-radius: 6px; border: 2px solid var(--bg); }
.message {
display: flex;
margin: 12px 0;
padding: 12px 18px;
max-width: 75%;
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: #f1f1f1;
color: var(--text);
border-bottom-left-radius: 4px;
padding-left: 40px;
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;
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); /* #1e1e1e */
color: var(--code-text); /* #d4d4d4 */
padding: 15px;
border-radius: 8px;
overflow-x: auto;
position: relative;
margin: 10px 0;
font-size: 16px;
line-height: 1.5;
}
.message-content code {
background: var(--code-bg); /* #1e1e1e */
color: var(--code-text); /* #d4d4d4 */
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
font-size: 16px;
}
.message-content pre code {
background: transparent;
color: var(--code-text);
padding: 0;
font-size: inherit;
}
.code-actions {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 8px;
background: rgba(255, 255, 255, 0.8);
padding: 6px;
border-radius: 6px;
z-index: 10;
opacity: 0;
transition: opacity 0.2s;
}
pre:hover .code-actions {
opacity: 1;
}
.code-actions button {
background: none;
color: #000000;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
}
.code-actions button:hover {
background: rgba(0, 0, 0, 0.1);
}
.loading-container {
display: flex;
justify-content: 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: #fff;
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;
}
#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-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;
}
@media (max-width: 600px) {
.container {
flex-direction: column;
height: auto;
padding-bottom: 100px;
}
.sidebar {
width: 80%;
max-width: none;
height: 100vh;
}
.chat-container {
flex: none;
margin: 0;
padding: 15px;
padding-bottom: 90px;
}
.input-container {
padding: 10px;
}
.input-wrapper {
padding: 10px;
}
.message { max-width: 90%; font-size: 16px; }
.menu-toggle { top: 5px; left: 5px; }
.message-content pre {
font-size: 14px;
padding: 12px;
}
.code-actions {
opacity: 1;
top: 8px;
right: 8px;
padding: 4px;
}
.code-actions button {
font-size: 12px;
padding: 4px 8px;
}
#message-input { font-size: 16px; }
.new-chat-btn, .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>
<div class="container">
<div class="sidebar">
<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>
<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,
highlight: (code, lang) => {
try {
const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
} catch (e) {
console.error("Highlighting error:", e);
return code;
}
}
});
initializeChat();
});
const Config = {
MAX_CONTEXT_LENGTH: 10,
MAX_MESSAGE_LENGTH: 65536,
MAX_IMAGE_UPLOADS: 5
};
let conversations = JSON.parse(localStorage.getItem('conversations')) || [];
let 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";
delete msg.images;
});
});
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', sendMessage);
stopButton.addEventListener('click', stopGenerating);
messageInput.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) e.preventDefault(), sendMessage();
});
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 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);
});
if (e.target.files.length > remainingSlots) {
alert(`最多只能上传${Config.MAX_IMAGE_UPLOADS}张图片,已自动截取前${remainingSlots}张`);
}
}
function handlePaste(e) {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const remainingSlots = Config.MAX_IMAGE_UPLOADS - uploadedImages.length;
if (remainingSlots <= 0) {
alert('最多只能上传5张图片!');
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++;
}
}
if (imageCount > remainingSlots) {
alert(`最多只能上传${Config.MAX_IMAGE_UPLOADS}张图片,已自动截取前${remainingSlots}张`);
}
}
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;
const conversation = conversations.find(c => c.id === id);
if (!conversation) return;
chatMessages.innerHTML = '';
conversation.messages.forEach(msg => addMessage(msg.parts[0].text, msg.role === 'user' ? 'user' : 'ai', null));
currentContext = conversation.messages.map(msg => ({ role: msg.role, parts: msg.parts }));
updateChatHistory();
sidebar.classList.remove('active');
menuToggle.classList.remove('hidden');
document.body.style.overflow = 'auto';
}
function addMessage(text, type = 'user', images = null) {
const existingMessage = chatMessages.querySelector(`.message.${type}:last-child`);
if (existingMessage && type === 'ai' && !images) {
const contentDiv = existingMessage.querySelector('.message-content');
contentDiv.innerHTML = marked.parse(text);
addCodeActions(contentDiv);
} else {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = marked.parse(text);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
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 = `<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().match(/^<!doctype html>|^<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;
navigator.clipboard.writeText(code).then(() => {
button.textContent = '已复制!';
setTimeout(() => button.textContent = '复制', 2000);
});
}
function previewHTML(button) {
const code = button.closest('pre').querySelector('code').textContent;
const iframe = htmlPreviewContent;
iframe.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 = '';
adjustInputHeight();
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(`HTTP error! status: ${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({ done, value }) {
if (done) {
// 更新上下文,包括图片
const userParts = [{"text": message || "请描述这些图片"}].concat(
imagesToSend.map(img => ({"inline_data": {"mime_type": "image/jpeg", "data": img}}))
);
currentContext.push(
{ role: 'user', parts: userParts },
{ role: 'model', parts: [{"text": accumulatedText}] }
);
currentContext = currentContext.slice(-Config.MAX_CONTEXT_LENGTH);
saveToHistory(message || "请描述这些图片", accumulatedText, imagesToSend);
imagePreview.innerHTML = '';
uploadedImages = [];
endGeneratingState();
eventSource = null;
return;
}
const chunk = decoder.decode(value);
chunk.split('\n\n').forEach(line => {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.text) {
accumulatedText += data.text;
contentDiv.innerHTML = marked.parse(accumulatedText);
addCodeActions(contentDiv);
} else if (data.error) {
contentDiv.innerHTML = `<span style="color: #666">${escape(data.error)}</span>`;
endGeneratingState();
eventSource.close();
eventSource = null;
}
} catch (e) {
console.error("Stream error:", e);
}
}
});
if (isGenerating) reader.read().then(processStream);
else {
reader.cancel();
endGeneratingState();
eventSource = null;
}
}
reader.read().then(processStream);
} catch (error) {
chatMessages.removeChild(loadingDiv);
addMessage(`错误: ${error.message}`, 'ai');
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) {
const conversation = conversations.find(c => c.id === currentConversationId) || { id: currentConversationId, messages: [] };
const userParts = [{"text": message}].concat(images.map(img => ({"inline_data": {"mime_type": "image/jpeg", "data": img}})));
if (!conversations.includes(conversation)) conversations.push(conversation);
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();
chatMessages.innerHTML = '';
imagePreview.innerHTML = '';
uploadedImages = [];
currentContext = [];
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"Render index error: {str(e)}")
return "Template loading failed", 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)