💻 源码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>智能问答系统</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
    <style>
        :root {
            --primary-color: #4a6fa5;
            --secondary-color: #6c757d;
            --accent-color: #61dafb;
            --bg-color: #f8f9fa;
            --text-color: #343a40;
            --border-color: #dee2e6;
            --hover-color: #e9ecef;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            color: var(--text-color);
            background-color: var(--bg-color);
            display: flex;
            flex-direction: column;
            height: 100vh;
        }

        header {
            background-color: var(--primary-color);
            color: white;
            padding: 1rem;
            text-align: center;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 1rem;
            display: flex;
            flex: 1;
            flex-direction: column;
        }

        .chat-container {
            display: flex;
            height: calc(100vh - 180px);
            border: 1px solid var(--border-color);
            border-radius: 8px;
            overflow: hidden;
            background-color: white;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }

        .sidebar {
            width: 250px;
            background-color: #f1f3f5;
            padding: 1rem;
            border-right: 1px solid var(--border-color);
            display: flex;
            flex-direction: column;
        }

        .main-content {
            flex: 1;
            display: flex;
            flex-direction: column;
            position: relative;
        }

        .chat-history {
            flex: 1;
            padding: 1rem;
            overflow-y: auto;
            scroll-behavior: smooth;
        }

        .chat-input-container {
            padding: 1rem;
            border-top: 1px solid var(--border-color);
            background-color: white;
            display: flex;
            flex-direction: column;
        }

        .source-filter {
            display: flex;
            align-items: center;
            margin-bottom: 0.5rem;
        }

        .source-filter select {
            margin-left: 0.5rem;
            padding: 0.3rem;
            border-radius: 4px;
            border: 1px solid var(--border-color);
        }

        .input-group {
            display: flex;
        }

        .chat-input {
            flex: 1;
            padding: 0.75rem;
            border: 1px solid var(--border-color);
            border-radius: 4px 0 0 4px;
            font-size: 1rem;
            resize: none;
            height: 45px;
            max-height: 150px;
            overflow-y: auto;
        }

        .chat-input:focus {
            outline: none;
            border-color: var(--primary-color);
        }

        .send-btn {
            background-color: var(--primary-color);
            color: white;
            border: none;
            padding: 0 1.25rem;
            border-radius: 0 4px 4px 0;
            cursor: pointer;
            font-size: 1rem;
            transition: background-color 0.2s;
        }

        .send-btn:hover {
            background-color: #3a5a8c;
        }

        .send-btn:disabled {
            background-color: var(--secondary-color);
            cursor: not-allowed;
        }

        .message {
            margin-bottom: 1rem;
            display: flex;
            flex-direction: column;
        }

        .message-content {
            padding: 0.75rem 1rem;
            border-radius: 8px;
            max-width: 80%;
            word-break: break-word;
        }

        .user-message {
            align-self: flex-end;
        }

        .user-message .message-content {
            background-color: var(--primary-color);
            color: white;
            align-self: flex-end;
        }

        .system-message {
            align-self: flex-start;
        }

        .system-message .message-content {
            background-color: #e9ecef;
            align-self: flex-start;
        }

        .typing-indicator {
            display: inline-block;
            padding: 0.75rem 1rem;
            background-color: #e9ecef;
            border-radius: 8px;
            margin-bottom: 1rem;
        }

        .typing-indicator span {
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background-color: var(--secondary-color);
            margin-right: 4px;
            animation: typing 1s infinite;
        }

        .typing-indicator span:nth-child(2) {
            animation-delay: 0.2s;
        }

        .typing-indicator span:nth-child(3) {
            animation-delay: 0.4s;
            margin-right: 0;
        }

        @keyframes typing {
            0%, 100% {
                transform: translateY(0);
            }
            50% {
                transform: translateY(-5px);
            }
        }

        .session-controls {
            display: flex;
            justify-content: space-between;
            margin-bottom: 1rem;
        }

        .new-session-btn, .clear-history-btn {
            background-color: var(--secondary-color);
            color: white;
            border: none;
            padding: 0.5rem 1rem;
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.9rem;
            transition: background-color 0.2s;
        }

        .new-session-btn:hover, .clear-history-btn:hover {
            background-color: #5a6268;
        }

        .session-info {
            font-size: 0.9rem;
            color: var(--secondary-color);
            margin-top: 1rem;
        }

        .history-title {
            font-weight: bold;
            margin-bottom: 1rem;
            color: var(--primary-color);
        }

        .history-list {
            flex: 1;
            overflow-y: auto;
        }

        .history-item {
            padding: 0.75rem;
            border-bottom: 1px solid var(--border-color);
            cursor: pointer;
            transition: background-color 0.2s;
        }

        .history-item:hover {
            background-color: var(--hover-color);
        }

        .history-question {
            font-weight: bold;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .history-answer {
            color: var(--secondary-color);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            font-size: 0.85rem;
        }

        .loading-overlay {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: rgba(255, 255, 255, 0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10;
            visibility: hidden;
        }

        .spinner {
            width: 40px;
            height: 40px;
            border: 4px solid rgba(0, 0, 0, 0.1);
            border-radius: 50%;
            border-left-color: var(--primary-color);
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        /* 响应式设计 */
        @media (max-width: 768px) {
            .chat-container {
                flex-direction: column;
            }

            .sidebar {
                width: 100%;
                border-right: none;
                border-bottom: 1px solid var(--border-color);
                max-height: 200px;
            }
        }
    </style>
</head>
<body>
    <header>
        <h1>智能问答系统</h1>
    </header>

    <div class="container">
        <div class="chat-container">
            <div class="sidebar">
                <div class="session-controls">
                    <button class="new-session-btn" id="newSessionBtn">
                        <i class="fas fa-plus"></i> 新会话
                    </button>
                    <button class="clear-history-btn" id="clearHistoryBtn">
                        <i class="fas fa-trash"></i> 清除历史
                    </button>
                </div>
                <div class="history-title">历史消息</div>
                <div class="history-list" id="historyList">
                    <!-- 历史消息将动态添加到这里 -->
                </div>
                <div class="session-info" id="sessionInfo"></div>
            </div>

            <div class="main-content">
                <div class="chat-history" id="chatHistory">
                    <!-- 消息将动态添加到这里 -->
                    <div class="message system-message">
                        <div class="message-content">
                            您好!我是智能问答助手,有什么我可以帮您的吗?
                        </div>
                    </div>
                </div>

                <div class="chat-input-container">
                    <div class="source-filter">
                        <label for="sourceFilter">学科类别:</label>
                        <select id="sourceFilter">
                            <option value="">全部</option>
                            <!-- 学科类别选项将动态添加 -->
                        </select>
                    </div>
                    <div class="input-group">
                        <textarea class="chat-input" id="chatInput" placeholder="请输入您的问题..."
                            rows="1" oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"></textarea>
                        <button class="send-btn" id="sendBtn">
                            <i class="fas fa-paper-plane"></i>
                        </button>
                    </div>
                </div>

                <div class="loading-overlay" id="loadingOverlay">
                    <div class="spinner"></div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // 全局变量
        let currentSessionId = null;
        let isResponseInProgress = false;
        let socket = null;

        // DOM元素
        const chatHistory = document.getElementById('chatHistory');
        const chatInput = document.getElementById('chatInput');
        const sendBtn = document.getElementById('sendBtn');
        const sourceFilter = document.getElementById('sourceFilter');
        const historyList = document.getElementById('historyList');
        const sessionInfo = document.getElementById('sessionInfo');
        const newSessionBtn = document.getElementById('newSessionBtn');
        const clearHistoryBtn = document.getElementById('clearHistoryBtn');
        const loadingOverlay = document.getElementById('loadingOverlay');

        // API URL前缀,根据部署环境调整
        const API_BASE_URL = '';  // 如果API和前端在同一域,可以留空

        // 页面加载完成后执行
        document.addEventListener('DOMContentLoaded', async () => {
            // 创建新会话
            await createNewSession();

            // 加载学科类别
            await loadSources();

            // 事件监听器
            sendBtn.addEventListener('click', sendMessage);
            chatInput.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    sendMessage();
                }
            });

            newSessionBtn.addEventListener('click', createNewSession);
            clearHistoryBtn.addEventListener('click', clearHistory);
        });

        // 创建新会话
        async function createNewSession() {
            try {
                const response = await fetch(`${API_BASE_URL}/api/create_session`, {
                    method: 'POST'
                });

                if (!response.ok) {
                    throw new Error('创建会话失败');
                }

                const data = await response.json();
                currentSessionId = data.session_id;

                // 更新会话信息
                sessionInfo.textContent = `会话ID: ${currentSessionId.slice(0, 8)}...`;

                // 清空聊天历史
                clearChatHistory();

                // 加载会话历史
                await loadHistory();

            } catch (error) {
                console.error('创建会话错误:', error);
                alert('创建会话失败,请刷新页面重试');
            }
        }

        // 加载学科类别
        async function loadSources() {
            try {
                const response = await fetch(`${API_BASE_URL}/api/sources`);

                if (!response.ok) {
                    throw new Error('获取学科类别失败');
                }

                const data = await response.json();

                // 清空现有选项
                sourceFilter.innerHTML = '<option value="">全部</option>';

                // 添加学科类别选项
                data.sources.forEach(source => {
                    const option = document.createElement('option');
                    option.value = source;
                    option.textContent = source;
                    sourceFilter.appendChild(option);
                });

            } catch (error) {
                console.error('加载学科类别错误:', error);
            }
        }

        // 加载历史消息
        async function loadHistory() {
            if (!currentSessionId) return;

            try {
                const response = await fetch(`${API_BASE_URL}/api/history/${currentSessionId}`);

                if (!response.ok) {
                    throw new Error('获取历史记录失败');
                }

                const data = await response.json();

                // 清空历史列表
                historyList.innerHTML = '';

                // 添加历史记录到侧边栏
                data.history.forEach((item, index) => {
                    const historyItem = document.createElement('div');
                    historyItem.className = 'history-item';
                    historyItem.innerHTML = `
                        <div class="history-question">${item.question}</div>
                        <div class="history-answer">${item.answer.substring(0, 50)}${item.answer.length > 50 ? '...' : ''}</div>
                    `;

                    // 点击历史记录项显示对话
                    historyItem.addEventListener('click', () => {
                        // 清空现有聊天记录
                        clearChatHistory();

                        // 添加用户问题
                        addMessage('user', item.question);

                        // 添加系统回答
                        addMessage('system', item.answer);

                        // 滚动到底部
                        scrollToBottom();
                    });

                    historyList.appendChild(historyItem);
                });

            } catch (error) {
                console.error('加载历史记录错误:', error);
            }
        }

        // 清除历史记录
        async function clearHistory() {
            if (!currentSessionId) return;

            try {
                const response = await fetch(`${API_BASE_URL}/api/history/${currentSessionId}`, {
                    method: 'DELETE'
                });

                if (!response.ok) {
                    throw new Error('清除历史记录失败');
                }

                // 清空历史列表
                historyList.innerHTML = '';

                // 清空聊天历史
                clearChatHistory();

                // 添加欢迎消息
                chatHistory.innerHTML = `
                    <div class="message system-message">
                        <div class="message-content">
                            历史记录已清除,有什么我可以帮您的吗?
                        </div>
                    </div>
                `;

            } catch (error) {
                console.error('清除历史记录错误:', error);
                alert('清除历史记录失败');
            }
        }

        // 清空聊天历史
        function clearChatHistory() {
            chatHistory.innerHTML = `
                <div class="message system-message">
                    <div class="message-content">
                        您好!我是智能问答助手,有什么我可以帮您的吗?
                    </div>
                </div>
            `;
        }

        // 发送消息
        async function sendMessage() {
            const query = chatInput.value.trim();
            const filter = sourceFilter.value;

            if (!query || isResponseInProgress) return;

            // 重置输入框
            chatInput.value = '';
            chatInput.style.height = 'auto';

            // 添加用户消息到聊天窗口
            addMessage('user', query);

            // 设置正在响应标志
            isResponseInProgress = true;

            // 添加正在输入指示器
            const typingIndicator = document.createElement('div');
            typingIndicator.className = 'typing-indicator';
            typingIndicator.innerHTML = '<span></span><span></span><span></span>';
            chatHistory.appendChild(typingIndicator);

            // 滚动到底部
            scrollToBottom();

            try {
                // 发送查询请求
                const response = await fetch(`${API_BASE_URL}/api/query`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        query: query,
                        source_filter: filter,
                        session_id: currentSessionId
                    })
                });

                if (!response.ok) {
                    throw new Error('查询失败');
                }

                const data = await response.json();

                // 移除输入指示器
                if (typingIndicator.parentNode) {
                    chatHistory.removeChild(typingIndicator);
                }

                // 如果需要流式输出,使用WebSocket
                if (data.is_streaming) {
                    // 创建系统消息容器,但内容为空
                    const messageElement = document.createElement('div');
                    messageElement.className = 'message system-message';

                    const contentElement = document.createElement('div');
                    contentElement.className = 'message-content';
                    contentElement.textContent = '';

                    messageElement.appendChild(contentElement);
                    chatHistory.appendChild(messageElement);

                    // 建立WebSocket连接进行流式输出
                    await handleStreamingResponse(query, filter, contentElement);
                } else {
                    // 直接添加完整答案
                    addMessage('system', data.answer);
                }

                // 重新加载历史记录
                await loadHistory();

            } catch (error) {
                console.error('发送消息错误:', error);

                // 移除输入指示器
                if (typingIndicator.parentNode) {
                    chatHistory.removeChild(typingIndicator);
                }

                // 添加错误消息
                addMessage('system', `抱歉,发生错误: ${error.message}`);
            } finally {
                // 清除正在响应标志
                isResponseInProgress = false;

                // 滚动到底部
                scrollToBottom();
            }
        }

        // 处理流式响应
        async function handleStreamingResponse(query, filter, contentElement) {
            // 关闭现有连接
            if (socket && socket.readyState === WebSocket.OPEN) {
                socket.close();
            }

            return new Promise((resolve, reject) => {
                // WebSocket URL
                const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
                const wsUrl = `${wsProtocol}//${window.location.host}${API_BASE_URL}/api/stream`;

                // 创建WebSocket连接
                socket = new WebSocket(wsUrl);

                // 连接建立时
                socket.onopen = function() {
                    console.log('WebSocket连接已建立');

                    // 发送查询请求
                    socket.send(JSON.stringify({
                        query: query,
                        source_filter: filter,
                        session_id: currentSessionId
                    }));
                };

                // 接收消息时
                socket.onmessage = function(event) {
                    const data = JSON.parse(event.data);

                    switch (data.type) {
                        case 'start':
                            console.log('流式响应开始');
                            break;

                        case 'token':
                            // 追加token到内容
                            contentElement.textContent += data.token;
                            scrollToBottom();
                            break;

                        case 'end':
                            console.log('流式响应结束');
                            socket.close();
                            resolve();
                            break;

                        case 'error':
                            console.error('流式响应错误:', data.error);
                            contentElement.textContent += `\n[错误: ${data.error}]`;
                            socket.close();
                            reject(new Error(data.error));
                            break;
                    }
                };

                // 发生错误时
                socket.onerror = function(error) {
                    console.error('WebSocket错误:', error);
                    contentElement.textContent += '\n[连接错误]';
                    reject(error);
                };

                // 连接关闭时
                socket.onclose = function() {
                    console.log('WebSocket连接已关闭');
                    resolve();
                };
            });
        }

        // 添加消息到聊天窗口
        function addMessage(type, content) {
            const messageElement = document.createElement('div');
            messageElement.className = `message ${type === 'user' ? 'user-message' : 'system-message'}`;

            const contentElement = document.createElement('div');
            contentElement.className = 'message-content';
            contentElement.textContent = content;

            messageElement.appendChild(contentElement);
            chatHistory.appendChild(messageElement);

            // 滚动到底部
            scrollToBottom();
        }

        // 滚动到底部
        function scrollToBottom() {
            chatHistory.scrollTop = chatHistory.scrollHeight;
        }
    </script>
</body>
</html>