当前位置: 首页 > ai >正文

Java 面试题训练助手 Web 版本

文章目录

  • 1. 后端服务 (app.py)
  • 2. 前端页面 (templates/index.html)
  • 3. 样式表 (static/style.css)
  • 使用说明

在这里插入图片描述
要将 Java 面试题训练助手重构为 Web 版本,我们可以使用 Flask 框架来搭建后端服务,并配合 HTML/CSS/JavaScript 构建前端界面。这样可以实现在浏览器中进行交互操作。
下面是完整的 Web 版本实现,包含前端页面和后端服务:

java_interview_web/
├── app.py           # 后端Flask服务
├── templates/
│   └── index.html   # 前端页面
├── static/
│   └── style.css    # 样式表
└── data/            # 数据存储目录├── interview_cache.pkl├── history_data.json└── interview_records.txt

1. 后端服务 (app.py)

from flask import Flask, render_template, request, jsonify, send_from_directory
import os
import json
import time
import threading
import urllib.request
import urllib.error
from collections import deque
from datetime import datetime
import hashlib
import pickle
import platform
from openai import OpenAI as DeepSeekClient
from openai import APIConnectionError, RateLimitError, APIStatusError, AuthenticationError
import logging# 配置日志
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',handlers=[logging.FileHandler("app.log"),logging.StreamHandler()]
)
logger = logging.getLogger(__name__)# 初始化Flask应用
app = Flask(__name__)# 确保数据目录存在
DATA_DIR = "data"
os.makedirs(DATA_DIR, exist_ok=True)# 配置参数 - 建议将API_KEY放在环境变量中 替换为你自己的Key
API_KEY = os.environ.get("DEEPSEEK_API_KEY", "sk-55133f0b231547108309c0626") 
MODEL = "deepseek-chat"
MAX_RETRY = 4
CACHE_EXPIRE_MIN = 180
HISTORY_SIZE = 50  # 增加历史记录容量
RECORD_MAX_SIZE = 10 * 1024 * 1024  # 记录文件最大10MB
CACHE_FILE = os.path.join(DATA_DIR, "interview_cache.pkl")
HISTORY_FILE = os.path.join(DATA_DIR, "history_data.json")
RECORD_FILE = os.path.join(DATA_DIR, "interview_records.txt")
SYSTEM_NAME = platform.system()# 专业提示词模板 - 更精确的指令
SYSTEM_PROMPT = ("你是资深Java技术专家(阿里P8级别),请清晰、准确地回答Java面试题。""答案应控制在500字以内,结构清晰,重点突出。""如果涉及代码,需提供完整可运行的示例,并附带必要注释。"
)# 初始化DeepSeek客户端
try:deepseek_client = DeepSeekClient(api_key=API_KEY,base_url="https://api.deepseek.com")
except Exception as e:logger.error(f"初始化DeepSeek客户端失败: {str(e)}")deepseek_client = Noneclass CacheEntry:"""增强型缓存条目"""def __init__(self, answer):self.answer = answerself.timestamp = time.time()self.access_count = 1def is_expired(self):return (time.time() - self.timestamp) > CACHE_EXPIRE_MIN * 60def update_access(self):self.access_count += 1self.timestamp = time.time()def to_dict(self):return {'answer': self.answer,'timestamp': self.timestamp,'access_count': self.access_count}@classmethoddef from_dict(cls, data):entry = cls(data['answer'])entry.timestamp = data['timestamp']entry.access_count = data['access_count']return entrydef get_current_datetime():"""优化时间格式显示"""now = datetime.now()return now.strftime(f"%Y年%m月%d日 %H:%M:%S")def format_answer(answer):"""格式化答案,确保代码块显示正确"""return answerdef get_question_hash(question):"""生成问题哈希值作为缓存键"""return hashlib.md5(question.encode('utf-8')).hexdigest()def load_cache():"""加载持久化缓存,增加错误处理"""if not os.path.exists(CACHE_FILE):return {}try:with open(CACHE_FILE, 'rb') as f:cache_data = pickle.load(f)# 兼容旧版本缓存格式if isinstance(cache_data, dict):# 检查是否是新格式first_key = next(iter(cache_data.keys()), None)if first_key and isinstance(cache_data[first_key], dict):# 转换为对象格式return {k: CacheEntry.from_dict(v) for k, v in cache_data.items()}return cache_dataexcept Exception as e:logger.error(f"加载缓存失败: {str(e)}")# 尝试备份损坏的缓存文件if os.path.exists(CACHE_FILE):try:os.rename(CACHE_FILE, f"{CACHE_FILE}.bak")logger.info(f"已备份损坏的缓存文件到 {CACHE_FILE}.bak")except Exception as e:logger.error(f"备份缓存文件失败: {str(e)}")return {}def save_cache(cache):"""保存缓存到文件,增加错误处理"""try:# 清理过期缓存cleaned_cache = {k: v for k, v in cache.items() if not v.is_expired()}# 转换为可序列化的格式serializable_cache = {k: v.to_dict() for k, v in cleaned_cache.items()}with open(CACHE_FILE, 'wb') as f:pickle.dump(serializable_cache, f)logger.info(f"已保存缓存,共 {len(cleaned_cache)} 条记录")except Exception as e:logger.error(f"保存缓存失败: {str(e)}")def load_history():"""加载历史记录,增加错误处理"""if not os.path.exists(HISTORY_FILE):return deque(maxlen=HISTORY_SIZE)try:with open(HISTORY_FILE, 'r', encoding='utf-8') as f:data = json.load(f)return deque(data, maxlen=HISTORY_SIZE)except Exception as e:logger.error(f"加载历史记录失败: {str(e)}")# 尝试备份损坏的历史文件if os.path.exists(HISTORY_FILE):try:os.rename(HISTORY_FILE, f"{HISTORY_FILE}.bak")logger.info(f"已备份损坏的历史文件到 {HISTORY_FILE}.bak")except Exception as e:logger.error(f"备份历史文件失败: {str(e)}")return deque(maxlen=HISTORY_SIZE)def save_history(history):"""保存历史记录,增加错误处理"""try:with open(HISTORY_FILE, 'w', encoding='utf-8') as f:json.dump(list(history), f, ensure_ascii=False, indent=2)logger.info(f"已保存历史记录,共 {len(history)} 条")except Exception as e:logger.error(f"保存历史记录失败: {str(e)}")def check_record_file_size():"""检查记录文件大小,超过限制则创建新文件"""if os.path.exists(RECORD_FILE) and os.path.getsize(RECORD_FILE) > RECORD_MAX_SIZE:timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")backup_file = f"{RECORD_FILE}.{timestamp}.bak"try:os.rename(RECORD_FILE, backup_file)logger.info(f"记录文件过大,已备份到 {backup_file}")return Trueexcept Exception as e:logger.error(f"备份记录文件失败: {str(e)}")return Falsereturn Truedef record_to_file(question, answer, timestamp):"""将问题和答案记录到本地txt文件,增加文件大小控制"""try:# 检查文件大小check_record_file_size()with open(RECORD_FILE, 'a', encoding='utf-8') as f:# 写入分隔线f.write("=" * 80 + "\n")# 写入时间f.write(f"记录时间: {timestamp}\n\n")# 写入问题f.write(f"问题: {question}\n\n")# 写入答案f.write(f"答案:\n{answer}\n\n")return True, os.path.abspath(RECORD_FILE)except Exception as e:logger.error(f"记录到文件失败: {str(e)}")return False, str(e)def check_internet():"""检查网络连接,增加多个检查点"""sites = ['https://www.baidu.com', 'https://www.aliyun.com', 'https://www.github.com']for site in sites:try:urllib.request.urlopen(site, timeout=3)return Trueexcept:continuereturn Falsedef get_ai_answer(question):"""使用DeepSeek官方SDK调用API,增强错误处理"""if not deepseek_client:raise Exception("DeepSeek客户端初始化失败,请检查API密钥")retry_delay = 1.5for attempt in range(MAX_RETRY):try:response = deepseek_client.chat.completions.create(model=MODEL,messages=[{"role": "system", "content": SYSTEM_PROMPT},{"role": "user", "content": question}],stream=False,timeout=30  # 设置超时时间)return response.choices[0].message.content.strip()except AuthenticationError:logger.error("API密钥验证失败")raise Exception("API密钥验证失败,请检查您的API密钥是否正确")except RateLimitError:wait_time = retry_delay * (attempt + 1)logger.warning(f"请求被限流,将在 {wait_time:.1f} 秒后重试")time.sleep(wait_time)continueexcept (APIConnectionError, TimeoutError) as e:if attempt < MAX_RETRY - 1:wait_time = retry_delay * (2 **attempt)logger.warning(f"网络错误: {str(e)},将在 {wait_time:.1f} 秒后重试")time.sleep(wait_time)continueraise ConnectionError(f"网络错误: {str(e)}") from eexcept APIStatusError as e:logger.error(f"API状态错误: HTTP {e.status_code} - {e.message}")if e.status_code == 429:wait_time = retry_delay * (attempt + 2)logger.warning(f"服务器限流,将在 {wait_time:.1f} 秒后重试")time.sleep(wait_time)continueraise Exception(f"API错误: HTTP {e.status_code} - {e.message}")except Exception as e:logger.error(f"获取答案时发生错误: {str(e)}")if attempt < MAX_RETRY - 1:wait_time = retry_delay * (attempt + 1)time.sleep(wait_time)continueraise Exception(f"请求失败: {str(e)}")raise Exception("API请求失败,超过最大重试次数")# 全局变量和锁(确保线程安全)
history_lock = threading.Lock()
cache_lock = threading.Lock()
history = load_history()
answer_cache = load_cache()# 路由
@app.route('/')
def index():"""主页面"""return render_template('index.html', current_time=get_current_datetime())@app.route('/api/ask', methods=['POST'])
def ask_question():"""处理问题请求,增加线程安全控制"""try:data = request.jsonquestion = data.get('question', '').strip()if not question:return jsonify({"status": "error", "message": "问题不能为空"})current_time = get_current_datetime()logger.info(f"收到问题: {question}")# 线程安全地操作历史记录with history_lock:record = {"time": current_time,"question": question}history.append(record)save_history(history)# 生成缓存键question_hash = get_question_hash(question)# 线程安全地检查缓存with cache_lock:cache_entry = answer_cache.get(question_hash)if cache_entry and not cache_entry.is_expired():cache_entry.update_access()save_cache(answer_cache)with history_lock:record["answer"] = cache_entry.answersave_history(history)# 记录到文件success, msg = record_to_file(question, cache_entry.answer, current_time)return jsonify({"status": "success","from_cache": True,"question": question,"answer": cache_entry.answer,"timestamp": current_time,"record_status": "success" if success else "failed","record_message": msg})# 获取新答案answer = get_ai_answer(question)# 线程安全地更新缓存with cache_lock:answer_cache[question_hash] = CacheEntry(answer)save_cache(answer_cache)# 线程安全地更新历史记录with history_lock:record["answer"] = answersave_history(history)# 记录到文件success, msg = record_to_file(question, answer, current_time)return jsonify({"status": "success","from_cache": False,"question": question,"answer": answer,"timestamp": current_time,"record_status": "success" if success else "failed","record_message": msg})except Exception as e:logger.error(f"处理问题时发生错误: {str(e)}")# 记录错误信息with history_lock:record["error"] = str(e)save_history(history)return jsonify({"status": "error","message": str(e),"question": question if 'question' in locals() else None})@app.route('/api/history', methods=['GET'])
def get_history():"""获取历史记录,增加分页支持"""try:page = request.args.get('page', 1, type=int)page_size = request.args.get('page_size', 10, type=int)with history_lock:history_list = list(history)total = len(history_list)# 分页处理start = (page - 1) * page_sizeend = start + page_sizepaginated = history_list[start:end]return jsonify({"status": "success","history": paginated,"pagination": {"total": total,"page": page,"page_size": page_size,"pages": (total + page_size - 1) // page_size}})except Exception as e:logger.error(f"获取历史记录失败: {str(e)}")return jsonify({"status": "error","message": str(e)})@app.route('/api/clear-history', methods=['POST'])
def clear_history():"""清空历史记录,增加确认机制"""try:with history_lock:global historyhistory = deque(maxlen=HISTORY_SIZE)save_history(history)logger.info("历史记录已清空")return jsonify({"status": "success", "message": "历史记录已清空"})except Exception as e:logger.error(f"清空历史记录失败: {str(e)}")return jsonify({"status": "error", "message": str(e)})@app.route('/api/check-network', methods=['GET'])
def check_network():"""检查网络连接"""try:status = check_internet()return jsonify({"status": "success", "connected": status})except Exception as e:logger.error(f"检查网络连接失败: {str(e)}")return jsonify({"status": "error", "message": str(e)})@app.route('/api/download-records', methods=['GET'])
def download_records():"""下载记录文件"""try:if not os.path.exists(RECORD_FILE):return jsonify({"status": "error", "message": "记录文件不存在"})directory = os.path.dirname(RECORD_FILE)filename = os.path.basename(RECORD_FILE)return send_from_directory(directory, filename, as_attachment=True)except Exception as e:logger.error(f"下载记录文件失败: {str(e)}")return jsonify({"status": "error", "message": str(e)})@app.route('/api/health', methods=['GET'])
def health_check():"""健康检查接口"""try:api_status = "正常" if deepseek_client else "未初始化"return jsonify({"status": "healthy","timestamp": get_current_datetime(),"api_status": api_status,"cache_size": len(answer_cache),"history_size": len(history)})except Exception as e:return jsonify({"status": "unhealthy","message": str(e)})if __name__ == '__main__':# 启动前检查API密钥if not API_KEY or API_KEY == "sk-55133f0b231547108309954bd6dc0626":logger.warning("使用默认API密钥,可能无法正常工作,请设置环境变量DEEPSEEK_API_KEY")# 启动服务,默认监听所有地址app.run(host='0.0.0.0', port=5000, debug=False)

2. 前端页面 (templates/index.html)

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Java面试题训练助手</title><link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body><div class="container"><header><h1>🚀 Java面试题训练助手</h1><p class="version">v2.5 (Web版) | {{ current_time }}</p></header><div class="status-bar"><div class="network-status"><span id="network-indicator" class="indicator"><i class="fas fa-wifi"></i> 检查网络连接...</span></div><div class="system-status"><span id="cache-indicator" class="indicator warning"><i class="fas fa-database"></i> 缓存加载中...</span></div></div><div class="main-content"><div class="question-section"><h2><i class="fas fa-question-circle"></i> 提问区域</h2><div class="question-tips"><i class="fas fa-lightbulb"></i> 提示:可输入"HashMap实现原理"或包含"代码"获取示例</div><textarea id="question-input" placeholder="请输入Java面试问题,例如:'HashMap实现原理'"></textarea><div class="button-group"><button id="ask-button" class="primary-btn"><i class="fas fa-paper-plane"></i> 提交问题</button><button id="clear-history-button" class="danger-btn"><i class="fas fa-trash"></i> 清空历史</button><button id="show-history-button" class="secondary-btn"><i class="fas fa-history"></i> 查看历史</button></div></div><div class="answer-section"><h2><i class="fas fa-comment-dots"></i> 答案区域</h2><div id="loading-indicator" class="loading"><div class="spinner"></div><p>思考中...</p></div><div id="answer-container"></div><div id="record-status" class="record-status"></div></div></div><!-- 历史记录模态框 --><div class="history-modal" id="history-modal"><div class="modal-content"><div class="modal-header"><h2><i class="fas fa-history"></i> 历史记录</h2><span class="close-button">&times;</span></div><div class="modal-body"><div class="history-filter"><input type="text" id="history-search" placeholder="搜索历史记录..."></div><div id="history-container"></div><div class="pagination"><button class="pagination-btn" id="prev-page" disabled><i class="fas fa-chevron-left"></i> 上一页</button><span id="page-info">1</span><button class="pagination-btn" id="next-page" disabled>下一页 <i class="fas fa-chevron-right"></i></button></div></div></div></div><!-- 详情模态框 --><div class="detail-modal" id="detail-modal"><div class="modal-content"><div class="modal-header"><h3><i class="fas fa-file-alt"></i> 详情</h3><span class="close-detail">&times;</span></div><div class="modal-body" id="detail-content"><!-- 详情内容将在这里显示 --></div></div></div><!-- 通知组件 --><div id="notification" class="notification"><i class="fas fa-info-circle"></i><span id="notification-message"></span></div><footer><p>所有问答将自动记录到本地文件 | 使用DeepSeek API</p></footer></div><script>// 全局变量let currentPage = 1;const itemsPerPage = 5;let allHistory = [];// 页面加载完成后执行document.addEventListener('DOMContentLoaded', function() {// 元素引用 - 使用更可靠的获取方式const questionInput = document.getElementById('question-input');const askButton = document.getElementById('ask-button');const clearHistoryButton = document.getElementById('clear-history-button');const showHistoryButton = document.getElementById('show-history-button');const answerContainer = document.getElementById('answer-container');const loadingIndicator = document.getElementById('loading-indicator');const recordStatus = document.getElementById('record-status');const historyModal = document.getElementById('history-modal');const historyContainer = document.getElementById('history-container');const historySearch = document.getElementById('history-search');const closeButton = document.querySelector('.close-button');const detailModal = document.getElementById('detail-modal');const detailContent = document.getElementById('detail-content');const closeDetail = document.querySelector('.close-detail');const prevPageBtn = document.getElementById('prev-page');const nextPageBtn = document.getElementById('next-page');const pageInfo = document.getElementById('page-info');const networkIndicator = document.getElementById('network-indicator');const cacheIndicator = document.getElementById('cache-indicator');const notification = document.getElementById('notification');const notificationMessage = document.getElementById('notification-message');// 确保模态框初始状态为隐藏historyModal.style.display = 'none';detailModal.style.display = 'none';loadingIndicator.style.display = 'none';// 检查网络状态checkNetworkStatus();// 加载缓存状态updateCacheStatus();// 模态框关闭功能 - 修复核心function closeHistoryModal() {historyModal.style.display = 'none';// 移除事件监听器防止多次触发document.removeEventListener('keydown', handleEscClose);}function closeDetailModal() {detailModal.style.display = 'none';}// ESC键关闭模态框function handleEscClose(e) {if (e.key === 'Escape') {closeHistoryModal();closeDetailModal();}}// 点击模态框外部关闭function handleOutsideClick(e) {if (e.target === historyModal) {closeHistoryModal();}if (e.target === detailModal) {closeDetailModal();}}// 关闭按钮事件 - 使用click事件而非onclick属性closeButton.addEventListener('click', closeHistoryModal);closeDetail.addEventListener('click', closeDetailModal);// 点击外部关闭window.addEventListener('click', handleOutsideClick);// 显示历史记录showHistoryButton.addEventListener('click', function() {currentPage = 1;loadHistory();historyModal.style.display = 'block';// 添加ESC键监听document.addEventListener('keydown', handleEscClose);});// 提交问题askButton.addEventListener('click', function() {submitQuestion();});// 键盘快捷键: Ctrl+Enter提交问题questionInput.addEventListener('keydown', function(e) {if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {e.preventDefault();submitQuestion();}});// 键盘快捷键: Alt+H显示历史document.addEventListener('keydown', function(e) {if (e.key === 'h' && e.altKey) {e.preventDefault();showHistoryButton.click();}});// 清空历史clearHistoryButton.addEventListener('click', function() {if (confirm('确定要清空所有历史记录吗?此操作不可恢复!')) {clearHistory();}});// 历史记录搜索historySearch.addEventListener('input', function() {currentPage = 1;loadHistory();});// 分页控制prevPageBtn.addEventListener('click', function() {if (currentPage > 1) {currentPage--;loadHistory();}});nextPageBtn.addEventListener('click', function() {const filteredHistory = getFilteredHistory();const totalPages = Math.ceil(filteredHistory.length / itemsPerPage);if (currentPage < totalPages) {currentPage++;loadHistory();}});// 提交问题函数function submitQuestion() {const question = questionInput.value.trim();if (!question) {showNotification('请输入问题内容', 'warning');return;}// 显示加载指示器loadingIndicator.style.display = 'flex';answerContainer.innerHTML = '';recordStatus.innerHTML = '';// 发送请求fetch('/api/ask', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ question: question })}).then(response => {if (!response.ok) {throw new Error(`HTTP错误状态: ${response.status}`);}return response.json();}).then(data => {// 隐藏加载指示器loadingIndicator.style.display = 'none';if (data.status === 'success') {// 显示答案let sourceLabel = data.from_cache ? '<div class="answer-source"><i class="fas fa-database"></i> 来自缓存</div>' : '<div class="answer-source"><i class="fas fa-sync-alt"></i> 实时生成</div>';answerContainer.innerHTML = `<div class="answer-header"><h3>问题: ${escapeHtml(data.question)}</h3><p class="timestamp"><i class="far fa-clock"></i> ${data.timestamp}</p>${sourceLabel}</div><div class="answer-content">${formatAnswer(escapeHtml(data.answer))}</div>`;// 显示记录状态if (data.record_status === 'success') {recordStatus.innerHTML = `<i class="fas fa-check-circle"></i> 已记录到文件: ${data.record_message}`;recordStatus.className = 'record-status success';} else {recordStatus.innerHTML = `<i class="fas fa-exclamation-circle"></i> 记录失败: ${data.record_message}`;recordStatus.className = 'record-status error';}// 清空输入框questionInput.value = '';showNotification('答案获取成功', 'success');} else {answerContainer.innerHTML = `<div class="error-message"><i class="fas fa-exclamation-triangle"></i> ${escapeHtml(data.message)}</div>`;showNotification(`获取失败: ${data.message}`, 'error');}}).catch(error => {loadingIndicator.style.display = 'none';answerContainer.innerHTML = `<div class="error-message"><i class="fas fa-exclamation-triangle"></i> 请求失败: ${escapeHtml(error.message)}</div>`;showNotification(`请求失败: ${error.message}`, 'error');});}// 加载历史记录function loadHistory() {const historyContainer = document.getElementById('history-container');historyContainer.innerHTML = '<div class="history-loading"><div class="spinner"></div><p>加载中...</p></div>';fetch('/api/history').then(response => response.json()).then(data => {if (data.status === 'success') {allHistory = data.history || [];const filteredHistory = getFilteredHistory();renderHistoryPage(filteredHistory);// 更新分页按钮状态updatePagination(filteredHistory.length);} else {historyContainer.innerHTML = `<p class="empty-history"><i class="fas fa-exclamation-circle"></i> 加载历史记录失败: ${data.message}</p>`;}}).catch(error => {historyContainer.innerHTML = `<p class="empty-history"><i class="fas fa-exclamation-circle"></i> 加载失败: ${error.message}</p>`;});}// 获取过滤后的历史记录function getFilteredHistory() {const searchTerm = historySearch.value.toLowerCase().trim();if (!searchTerm) return allHistory;return allHistory.filter(item => item.question.toLowerCase().includes(searchTerm) || (item.answer && item.answer.toLowerCase().includes(searchTerm)));}// 渲染当前页的历史记录function renderHistoryPage(history) {const historyContainer = document.getElementById('history-container');const startIndex = (currentPage - 1) * itemsPerPage;const endIndex = startIndex + itemsPerPage;const pageItems = history.slice(startIndex, endIndex);if (pageItems.length === 0) {historyContainer.innerHTML = '<p class="empty-history"><i class="fas fa-search"></i> 没有找到匹配的记录</p>';return;}let html = '';pageItems.forEach((item, index) => {const globalIndex = startIndex + index + 1;const answerPreview = item.answer ? item.answer.length > 100 ? item.answer.substring(0, 100) + '...' : item.answer : '<span class="no-answer">无答案</span>';html += `<div class="history-item" data-id="${globalIndex}"><div class="history-header"><span class="history-index">${globalIndex}</span><span class="history-time">${item.time}</span></div><div class="history-question">${escapeHtml(item.question)}</div><div class="history-answer">${escapeHtml(answerPreview)}</div><div class="history-actions"><button class="view-detail-btn" data-index="${history.indexOf(item)}"><i class="fas fa-eye"></i> 查看详情</button><button class="repeat-question-btn" data-question="${escapeHtml(item.question)}"><i class="fas fa-redo"></i> 重复提问</button></div></div>`;});historyContainer.innerHTML = html;// 为查看详情按钮添加事件监听document.querySelectorAll('.view-detail-btn').forEach(btn => {btn.addEventListener('click', function() {const index = parseInt(this.getAttribute('data-index'));showDetail(allHistory[index]);});});// 为重复提问按钮添加事件监听document.querySelectorAll('.repeat-question-btn').forEach(btn => {btn.addEventListener('click', function() {const question = this.getAttribute('data-question');questionInput.value = question;closeHistoryModal();// 滚动到问题输入框questionInput.scrollIntoView({ behavior: 'smooth' });});});}// 更新分页状态function updatePagination(totalItems) {const totalPages = Math.ceil(totalItems / itemsPerPage);pageInfo.textContent = `第 ${currentPage} / ${totalPages} 页`;prevPageBtn.disabled = currentPage <= 1;nextPageBtn.disabled = currentPage >= totalPages;}// 显示详情function showDetail(item) {if (!item) return;const answer = item.answer || '<span class="no-answer">无答案</span>';detailContent.innerHTML = `<div class="detail-item"><strong>时间</strong><div>${item.time}</div></div><div class="detail-item"><strong>问题</strong><div>${escapeHtml(item.question)}</div></div><div class="detail-item"><strong>答案</strong><div class="detail-answer">${formatAnswer(escapeHtml(answer))}</div></div>`;detailModal.style.display = 'block';}// 清空历史记录function clearHistory() {fetch('/api/clear-history', { method: 'POST' }).then(response => response.json()).then(data => {showNotification(data.message, 'info');loadHistory();}).catch(error => {showNotification(`清空失败: ${error.message}`, 'error');});}// 检查网络状态function checkNetworkStatus() {fetch('/api/check-network').then(response => response.json()).then(data => {if (data.connected) {networkIndicator.innerHTML = '<i class="fas fa-wifi"></i> 网络连接正常';networkIndicator.className = 'indicator online';} else {networkIndicator.innerHTML = '<i class="fas fa-wifi-slash"></i> 网络连接异常';networkIndicator.className = 'indicator offline';showNotification('网络连接异常,部分功能可能无法使用', 'warning');}}).catch(() => {networkIndicator.innerHTML = '<i class="fas fa-wifi-slash"></i> 网络连接异常';networkIndicator.className = 'indicator offline';showNotification('网络连接异常,部分功能可能无法使用', 'warning');});}// 更新缓存状态function updateCacheStatus() {cacheIndicator.innerHTML = '<i class="fas fa-database"></i> 缓存加载完成';cacheIndicator.className = 'indicator online';}// 显示通知function showNotification(message, type = 'info') {notificationMessage.textContent = message;notification.className = `notification ${type}`;notification.classList.add('show');// 3秒后隐藏通知setTimeout(() => {notification.classList.remove('show');}, 3000);}// 格式化答案(处理换行和代码块)function formatAnswer(answer) {// 处理换行answer = answer.replace(/\n/g, '<br>');// 处理代码块answer = answer.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');return answer;}// HTML转义,防止XSS攻击function escapeHtml(text) {if (!text) return '';return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');}});</script>
</body>
</html>

3. 样式表 (static/style.css)

:root {--primary-color: #3498db;--primary-dark: #2980b9;--secondary-color: #2ecc71;--secondary-dark: #27ae60;--danger-color: #e74c3c;--danger-dark: #c0392b;--warning-color: #f39c12;--warning-dark: #d35400;--text-color: #333;--text-light: #7f8c8d;--border-color: #e0e0e0;--background-color: #f5f7fa;--card-background: #ffffff;--shadow: 0 2px 10px rgba(0, 0, 0, 0.05);--shadow-hover: 0 5px 15px rgba(0, 0, 0, 0.1);--transition: all 0.3s ease;
}* {box-sizing: border-box;margin: 0;padding: 0;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}body {background-color: var(--background-color);color: var(--text-color);line-height: 1.6;padding-bottom: 60px;
}.container {max-width: 1200px;margin: 0 auto;padding: 20px;
}header {text-align: center;margin-bottom: 30px;padding-bottom: 20px;border-bottom: 1px solid var(--border-color);
}header h1 {color: var(--primary-color);margin-bottom: 10px;font-size: 2rem;
}.version {color: var(--text-light);font-style: italic;
}.status-bar {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20px;flex-wrap: wrap;gap: 10px;
}.network-status, .system-status {display: flex;gap: 10px;
}.indicator {padding: 6px 12px;border-radius: 20px;font-size: 0.9em;display: inline-flex;align-items: center;gap: 6px;
}.online {background-color: #eafaf1;color: #27ae60;
}.offline {background-color: #fdedeb;color: #e74c3c;
}.warning {background-color: #fef5e7;color: #d35400;
}.main-content {display: grid;grid-template-columns: 1fr 1fr;gap: 30px;
}@media (max-width: 768px) {.main-content {grid-template-columns: 1fr;}.status-bar {flex-direction: column;align-items: flex-start;}
}.question-section, .answer-section {background-color: var(--card-background);border-radius: 8px;padding: 20px;box-shadow: var(--shadow);transition: var(--transition);
}.question-section:hover, .answer-section:hover {box-shadow: var(--shadow-hover);
}h2 {color: var(--primary-color);margin-bottom: 15px;padding-bottom: 10px;border-bottom: 1px solid var(--border-color);display: flex;align-items: center;gap: 10px;
}.question-tips {background-color: #f8f9fa;padding: 10px 15px;border-radius: 4px;margin-bottom: 15px;font-size: 0.9em;color: var(--text-light);border-left: 3px solid var(--warning-color);
}#question-input {width: 100%;min-height: 120px;padding: 12px;border: 1px solid var(--border-color);border-radius: 4px;resize: vertical;font-size: 1em;margin-bottom: 15px;transition: var(--transition);
}#question-input:focus {outline: none;border-color: var(--primary-color);box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}.button-group {display: flex;gap: 10px;flex-wrap: wrap;
}button {padding: 10px 20px;border: none;border-radius: 4px;cursor: pointer;font-size: 1em;transition: var(--transition);display: inline-flex;align-items: center;gap: 8px;
}.primary-btn {background-color: var(--primary-color);color: white;flex-grow: 1;
}.primary-btn:hover {background-color: var(--primary-dark);
}.secondary-btn {background-color: var(--secondary-color);color: white;
}.secondary-btn:hover {background-color: var(--secondary-dark);
}.danger-btn {background-color: var(--danger-color);color: white;
}.danger-btn:hover {background-color: var(--danger-dark);
}#answer-container {min-height: 200px;padding: 15px;border: 1px solid #eee;border-radius: 4px;margin-bottom: 15px;line-height: 1.8;background-color: #fafafa;
}.answer-header {margin-bottom: 15px;padding-bottom: 10px;border-bottom: 1px solid #eee;
}.answer-header h3 {color: var(--primary-dark);margin-bottom: 5px;font-size: 1.1em;
}.timestamp {color: var(--text-light);font-size: 0.9em;display: flex;align-items: center;gap: 5px;
}.answer-source {display: inline-block;padding: 3px 10px;background-color: #f1c40f;color: #333;border-radius: 12px;font-size: 0.8em;margin-top: 5px;
}.answer-content {white-space: pre-wrap;
}/* 代码块样式 */
pre {background-color: #f8f8f8;border-radius: 4px;padding: 12px;margin: 10px 0;overflow-x: auto;font-family: 'Consolas', 'Monaco', monospace;
}code {font-family: 'Consolas', 'Monaco', monospace;font-size: 0.9em;color: #333;
}.loading {display: none;align-items: center;justify-content: center;flex-direction: column;padding: 40px 0;
}.spinner {width: 40px;height: 40px;border: 4px solid #f3f3f3;border-top: 4px solid var(--primary-color);border-radius: 50%;animation: spin 1s linear infinite;margin-bottom: 15px;
}@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }
}.record-status {padding: 10px;border-radius: 4px;font-size: 0.9em;margin-top: 10px;display: flex;align-items: center;gap: 8px;
}.success {background-color: #eafaf1;color: #27ae60;
}.error {background-color: #fdedeb;color: #e74c3c;
}.error-message {color: var(--danger-color);padding: 20px;text-align: center;border: 1px solid #fdedeb;border-radius: 4px;background-color: #fdedeb;
}/* 历史记录模态框 */
.history-modal, .detail-modal {display: none;position: fixed;z-index: 1000;left: 0;top: 0;width: 100%;height: 100%;overflow: auto;background-color: rgba(0, 0, 0, 0.4);backdrop-filter: blur(3px);animation: fadeIn 0.3s;
}@keyframes fadeIn {from { opacity: 0; }to { opacity: 1; }
}.modal-content {background-color: var(--card-background);margin: 5% auto;padding: 0;border: 1px solid #888;width: 90%;max-width: 900px;border-radius: 8px;max-height: 80vh;overflow: hidden;display: flex;flex-direction: column;box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}.modal-header {padding: 15px 20px;background-color: var(--primary-color);color: white;display: flex;justify-content: space-between;align-items: center;
}.modal-header h2, .modal-header h3 {color: white;margin: 0;padding: 0;border: none;
}.modal-body {padding: 20px;overflow-y: auto;flex-grow: 1;
}.close-button, .close-detail {color: white;font-size: 28px;font-weight: bold;cursor: pointer;transition: var(--transition);
}.close-button:hover, .close-detail:hover {color: #ddd;transform: scale(1.1);
}#history-container {margin-top: 20px;
}.history-item {padding: 15px;border-bottom: 1px solid #eee;margin-bottom: 15px;border-radius: 4px;transition: var(--transition);
}.history-item:hover {background-color: #f9f9f9;transform: translateX(5px);
}.history-header {display: flex;justify-content: space-between;margin-bottom: 10px;
}.history-index {font-weight: bold;color: var(--primary-color);background-color: rgba(52, 152, 219, 0.1);width: 24px;height: 24px;border-radius: 50%;display: inline-flex;align-items: center;justify-content: center;font-size: 0.9em;
}.history-time {font-size: 0.8em;color: var(--text-light);
}.history-question {font-weight: bold;margin-bottom: 8px;color: var(--primary-dark);word-break: break-word;
}.history-answer {color: var(--text-light);font-size: 0.9em;margin-bottom: 10px;word-break: break-word;
}.no-answer {color: var(--warning-color);font-style: italic;
}.history-actions {display: flex;gap: 10px;margin-top: 10px;
}.view-detail-btn, .repeat-question-btn {padding: 5px 10px;font-size: 0.8em;background-color: transparent;border: 1px solid var(--primary-color);color: var(--primary-color);
}.view-detail-btn:hover, .repeat-question-btn:hover {background-color: var(--primary-color);color: white;
}.empty-history {text-align: center;padding: 40px 0;color: var(--text-light);border: 1px dashed #ddd;border-radius: 4px;margin-top: 20px;
}.history-loading {display: flex;align-items: center;justify-content: center;flex-direction: column;padding: 40px 0;
}.history-filter {margin-bottom: 20px;
}#history-search {width: 100%;padding: 10px;border: 1px solid var(--border-color);border-radius: 4px;font-size: 1em;
}#history-search:focus {outline: none;border-color: var(--primary-color);
}.pagination {display: flex;justify-content: center;align-items: center;gap: 15px;margin-top: 20px;padding-top: 10px;border-top: 1px solid #eee;
}.pagination-btn {padding: 6px 12px;background-color: white;border: 1px solid var(--border-color);color: var(--text-color);
}.pagination-btn:hover:not(:disabled) {background-color: #f5f5f5;
}.pagination-btn:disabled {opacity: 0.5;cursor: not-allowed;
}/* 详情模态框样式 */
.detail-item {margin-bottom: 20px;
}.detail-item strong {display: block;margin-bottom: 5px;color: var(--primary-dark);
}.detail-answer {padding: 10px;background-color: #f9f9f9;border-radius: 4px;line-height: 1.8;
}/* 通知样式 */
.notification {position: fixed;bottom: 20px;right: 20px;padding: 12px 20px;border-radius: 4px;color: white;z-index: 9999;transform: translateY(100px);opacity: 0;transition: all 0.3s ease;box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);display: inline-flex;align-items: center;gap: 10px;
}.notification.show {transform: translateY(0);opacity: 1;
}.notification.info {background-color: var(--primary-color);
}.notification.success {background-color: var(--secondary-color);
}.notification.error {background-color: var(--danger-color);
}.notification.warning {background-color: var(--warning-color);
}footer {text-align: center;margin-top: 40px;padding-top: 20px;border-top: 1px solid var(--border-color);color: var(--text-light);font-size: 0.9em;
}

使用说明

1.** 安装依赖 **
bash
pip install flask openai

2.** 运行程序 **:
bash
python app.py

3.** 访问应用 **:
打开浏览器,访问 http://127.0.0.1:5000

http://www.xdnf.cn/news/18405.html

相关文章:

  • JavaScript 操作 DOM
  • php apache无法接收到Authorization header
  • express+mongoose的node部署
  • 优考试局域网系统V6.0.0版
  • AI 论文周报丨多模态记忆智能体/视觉基础模型/推理模型等多领域成果一键速览
  • AI服务器介绍
  • 《Linux 网络编程一:网络编程导论及UDP 服务器的创建与数据接收》
  • 《基于大数据的农产品交易数据分析与可视化系统》选题不当,毕业答辩可能直接挂科
  • Linux系统 --- 指令
  • tauri配置允许执行eval脚本,在打包cocos游戏web/phone移动端的时候一定要配置
  • yolo训练实例(一)
  • AAA 服务器与 RADIUS 协议笔记
  • C++函数重载与引用详解
  • Django中间件自定义开发指南:从原理到实战的深度解析
  • 【机器学习深度学习】vLLM的核心优化技术详解
  • 大型语言模型中奖励模型的原理:训练、打分与更新
  • Java面试-自动装箱与拆箱机制解析
  • 零知开源——基于ESP8266(ESP-12F)驱动YS-IR05F红外控制空调
  • pytorch 网络可视化
  • Electron 核心 API 全解析:从基础到实战场景
  • k8sday14数据存储(2/2)
  • RSS与今日头条技术对比分析
  • 代码随想录刷题Day40
  • Linux 软件包安装和管理的相关操作及使用总结(未完成)
  • 漏洞分析 | Kafka Connect 任意文件读取漏洞(CVE-2025-27817)
  • 如何使用AI大语言模型解决生活中的实际小事情?
  • 【Protues仿真】基于AT89C52单片机的LCD液晶显示屏显示控制
  • 如何在 Axios 中处理多个 baseURL 而不造成混乱
  • portainer-ce汉化版下载
  • 从零开始的云计算生活——第四十九天,长路漫漫,kubernetes模块之持久化存储