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)

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐