| {% extends "base.html" %} |
|
|
| {% block title %}{{ article.title }} - 个人博客{% endblock %} |
|
|
| {% block extra_css %} |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"> |
| <style> |
| :root { |
| --primary-blue: #4A90E2; |
| --light-blue: #63C2DE; |
| --soft-purple: #9B59B6; |
| --text-dark: #2C3E50; |
| --warm-cream: #FFF9E6; |
| } |
| |
| |
| .article-container { |
| max-width: 110vh; |
| margin: 0 auto; |
| background: white; |
| border-radius: 20px; |
| box-shadow: 0 2px 12px rgba(99, 145, 197, 0.08); |
| border: 2px solid var(--light-blue); |
| padding: 2.5rem; |
| } |
| |
| |
| .article-header { |
| margin-bottom: 2.5rem; |
| padding-bottom: 1.5rem; |
| border-bottom: 1px solid var(--light-blue); |
| } |
| |
| .article-title { |
| font-size: 2.5rem; |
| font-weight: 700; |
| color: var(--text-dark); |
| line-height: 1.3; |
| margin-bottom: 1rem; |
| background: linear-gradient(135deg, var(--primary-blue), var(--soft-purple)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .article-meta { |
| display: flex; |
| align-items: center; |
| gap: 1.5rem; |
| color: #64748B; |
| } |
| |
| .meta-item { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| |
| .meta-item i { |
| color: var(--primary-blue); |
| } |
| |
| |
| .article-summary { |
| background: var(--warm-cream); |
| border-radius: 16px; |
| padding: 1.5rem; |
| margin: 2rem 0; |
| position: relative; |
| } |
| |
| .summary-label { |
| position: absolute; |
| top: -12px; |
| left: 16px; |
| background: var(--primary-blue); |
| color: white; |
| padding: 0.25rem 1rem; |
| border-radius: 20px; |
| font-size: 0.875rem; |
| font-weight: 500; |
| } |
| |
| |
| .article-content { |
| line-height: 1.8; |
| color: var(--text-dark); |
| } |
| |
| .markdown-body { |
| font-size: 1.1rem; |
| } |
| |
| .markdown-body h1, |
| .markdown-body h2, |
| .markdown-body h3 { |
| color: var(--primary-blue); |
| margin-top: 2em; |
| margin-bottom: 1em; |
| font-weight: 600; |
| } |
| |
| .markdown-body p { |
| margin-bottom: 1.5em; |
| } |
| |
| .markdown-body a { |
| color: var(--primary-blue); |
| text-decoration: none; |
| border-bottom: 1px dashed var(--light-blue); |
| transition: all 0.3s; |
| } |
| |
| .markdown-body a:hover { |
| border-bottom-style: solid; |
| color: var(--soft-purple); |
| } |
| |
| .markdown-body code { |
| background: #F8FAFC; |
| padding: 0.2em 0.4em; |
| border-radius: 4px; |
| font-size: 0.9em; |
| color: var(--primary-blue); |
| } |
| |
| .markdown-body pre { |
| background: #F8FAFC; |
| border-radius: 12px; |
| padding: 1rem; |
| overflow-x: auto; |
| border: 1px solid var(--light-blue); |
| } |
| |
| .markdown-body pre code { |
| background: none; |
| padding: 0; |
| color: inherit; |
| } |
| |
| .markdown-body blockquote { |
| border-left: 4px solid var(--light-blue); |
| padding: 0.5rem 0 0.5rem 1rem; |
| margin: 1.5rem 0; |
| color: #64748B; |
| background: #F8FAFC; |
| } |
| |
| .markdown-body img { |
| max-width: 100%; |
| border-radius: 12px; |
| margin: 1.5rem 0; |
| } |
| |
| |
| .chat-toggle { |
| position: fixed; |
| right: 2rem; |
| bottom: 2rem; |
| width: 56px; |
| height: 56px; |
| border-radius: 28px; |
| background: linear-gradient(135deg, var(--primary-blue), var(--light-blue)); |
| color: white; |
| border: none; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 1.5rem; |
| box-shadow: 0 4px 12px rgba(99, 145, 197, 0.2); |
| transition: all 0.3s; |
| z-index: 998; |
| } |
| |
| .chat-toggle:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 6px 16px rgba(99, 145, 197, 0.3); |
| } |
| |
| .chat-window { |
| position: fixed; |
| right: 2rem; |
| bottom: 2rem; |
| width: 380px; |
| height: 600px; |
| background: white; |
| border-radius: 20px; |
| box-shadow: 0 4px 20px rgba(99, 145, 197, 0.15); |
| display: flex; |
| flex-direction: column; |
| transform: scale(0); |
| opacity: 0; |
| transform-origin: bottom right; |
| transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); |
| z-index: 999; |
| border: 2px solid var(--light-blue); |
| } |
| |
| .chat-window.active { |
| transform: scale(1); |
| opacity: 1; |
| } |
| |
| .chat-header { |
| padding: 1.25rem; |
| background: linear-gradient(135deg, var(--primary-blue), var(--light-blue)); |
| color: white; |
| border-radius: 20px 20px 0 0; |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| } |
| |
| .chat-title { |
| font-weight: 600; |
| flex: 1; |
| } |
| |
| .chat-close { |
| background: none; |
| border: none; |
| color: white; |
| cursor: pointer; |
| width: 32px; |
| height: 32px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| border-radius: 16px; |
| transition: all 0.3s; |
| } |
| |
| .chat-close:hover { |
| background: rgba(255, 255, 255, 0.2); |
| } |
| |
| |
| .chat-messages { |
| flex: 1; |
| overflow-y: auto; |
| overflow-x: hidden; |
| padding: 1.5rem; |
| display: flex; |
| flex-direction: column; |
| gap: 1rem; |
| background: #f8f9fa; |
| } |
| |
| .chat-message { |
| max-width: 85%; |
| padding: 1rem 1.25rem; |
| border-radius: 16px; |
| line-height: 1.5; |
| animation: messageSlide 0.3s ease; |
| word-wrap: break-word; |
| overflow-wrap: break-word; |
| width: fit-content; |
| border: 2px solid var(--light-blue); |
| background: white; |
| color: var(--text-dark); |
| box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); |
| } |
| |
| .chat-message.user { |
| margin-left: auto; |
| border-radius: 16px 16px 4px 16px; |
| background: #f8f9fa; |
| } |
| |
| .chat-message.assistant { |
| margin-right: auto; |
| border-radius: 16px 16px 16px 4px; |
| } |
| |
| .chat-message p { |
| margin: 0; |
| margin-bottom: 0.75rem; |
| } |
| |
| .chat-message p:last-child { |
| margin-bottom: 0; |
| } |
| |
| |
| .chat-message pre { |
| background: #f1f5f9; |
| border-radius: 8px; |
| padding: 1rem; |
| margin: 0.75rem 0; |
| overflow-x: auto; |
| border: 1px solid var(--light-blue); |
| } |
| |
| .chat-message pre code { |
| font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; |
| font-size: 0.9rem; |
| line-height: 1.5; |
| color: var(--text-dark); |
| background: transparent; |
| padding: 0; |
| } |
| |
| .chat-message code { |
| background: #f1f5f9; |
| padding: 0.2em 0.4em; |
| border-radius: 4px; |
| font-size: 0.9em; |
| color: var(--primary-blue); |
| font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; |
| } |
| |
| .chat-message .katex-display { |
| margin: 0.75rem 0; |
| overflow-x: auto; |
| padding: 0.5rem 0; |
| } |
| |
| .chat-message .katex { |
| font-size: 1.1em; |
| } |
| |
| .chat-input-container { |
| padding: 1.25rem; |
| border-top: 1px solid var(--light-blue); |
| } |
| |
| .chat-input-wrapper { |
| display: flex; |
| gap: 0.75rem; |
| align-items: flex-end; |
| } |
| |
| .chat-input { |
| flex: 1; |
| min-height: 44px; |
| max-height: 120px; |
| padding: 0.75rem 1rem; |
| border: 2px solid var(--light-blue); |
| border-radius: 12px; |
| resize: none; |
| font-size: 1rem; |
| line-height: 1.5; |
| transition: all 0.3s; |
| } |
| |
| .chat-input:focus { |
| outline: none; |
| border-color: var(--primary-blue); |
| box-shadow: 0 0 0 3px rgba(99, 145, 197, 0.1); |
| } |
| |
| .chat-send { |
| background: var(--primary-blue); |
| color: white; |
| width: 44px; |
| height: 44px; |
| border: none; |
| border-radius: 12px; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| transition: all 0.3s; |
| } |
| |
| .chat-send:hover { |
| background: var(--light-blue); |
| transform: translateY(-2px); |
| } |
| |
| |
| @keyframes messageSlide { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| |
| @media (max-width: 768px) { |
| |
| .article-container { |
| max-width: 60vh; |
| padding: 1.25rem; |
| border-radius: 12px; |
| margin: 1rem; |
| border-width: 1px; |
| } |
| |
| .article-header { |
| margin-bottom: 1.5rem; |
| padding-bottom: 1rem; |
| } |
| |
| .article-title { |
| font-size: 1.75rem; |
| line-height: 1.4; |
| } |
| |
| .article-meta { |
| flex-wrap: wrap; |
| gap: 1rem; |
| } |
| |
| .article-summary { |
| margin: 1.5rem 0; |
| padding: 1.25rem; |
| } |
| |
| |
| .chat-window { |
| position: fixed; |
| bottom: 20px; |
| width: 65vh; |
| height: 90vh; |
| left: 0; |
| right: 0; |
| margin-left: auto; |
| margin-right: auto; |
| border-radius: 20px 20px 0 0; |
| transform-origin: bottom center; |
| } |
| |
| .chat-messages { |
| padding: 1rem; |
| } |
| |
| .chat-message { |
| max-width: 90%; |
| padding: 0.875rem 1rem; |
| font-size: 0.95rem; |
| } |
| |
| .chat-message pre { |
| margin: 0.5rem 0; |
| padding: 0.875rem; |
| font-size: 0.85rem; |
| } |
| |
| .chat-input-wrapper { |
| padding: 0.875rem; |
| } |
| |
| .chat-input { |
| min-height: 40px; |
| padding: 0.625rem 0.875rem; |
| font-size: 0.95rem; |
| } |
| |
| .chat-send { |
| width: 40px; |
| height: 40px; |
| } |
| |
| .chat-toggle { |
| right: 1rem; |
| bottom: 1rem; |
| width: 48px; |
| height: 48px; |
| font-size: 1.25rem; |
| } |
| |
| |
| .chat-message, |
| .chat-input, |
| .chat-send, |
| .chat-toggle { |
| touch-action: manipulation; |
| -webkit-tap-highlight-color: transparent; |
| } |
| |
| |
| .markdown-body { |
| font-size: 1rem; |
| line-height: 1.6; |
| } |
| |
| .markdown-body pre { |
| -webkit-overflow-scrolling: touch; |
| } |
| |
| @media (max-height: 600px) { |
| .chat-window { |
| height: 85vh; |
| width: 65vh; |
| } |
| } |
| } |
| </style> |
| {% endblock %} |
|
|
| {% block content %} |
| |
| <article class="article-container"> |
| <header class="article-header"> |
| <h1 class="article-title">{{ article.title }}</h1> |
| <div class="article-meta"> |
| <div class="meta-item"> |
| <i class="fas fa-calendar"></i> |
| <span>{{ article.created_at.strftime('%Y-%m-%d') }}</span> |
| </div> |
| </div> |
| </header> |
|
|
| {% if article.summary %} |
| <div class="article-summary"> |
| <span class="summary-label">AI 摘要</span> |
| <p>{{ article.summary }}</p> |
| </div> |
| {% endif %} |
|
|
| <div class="article-content markdown-body"> |
| {{ article.content|markdown }} |
| </div> |
| </article> |
|
|
| |
| <button class="chat-toggle" id="chatToggle"> |
| <i class="fas fa-robot"></i> |
| </button> |
|
|
| <div class="chat-window" id="chatWindow"> |
| <div class="chat-header"> |
| <i class="fas fa-robot"></i> |
| <span class="chat-title">AI 智能助手</span> |
| <button class="chat-close" id="chatClose"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div class="chat-messages" id="chatMessages"></div> |
| <div class="chat-input-wrapper"> |
| <textarea |
| id="chatInput" |
| class="chat-input" |
| placeholder="输入您的问题..." |
| rows="1" |
| ></textarea> |
| <button class="chat-send" onclick="sendMessage()"> |
| <i class="fas fa-paper-plane"></i> |
| </button> |
| </div> |
| </div> |
| {% endblock %} |
|
|
| {% block extra_js %} |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
| <script> |
| |
| window.articleContext = { |
| title: {{ article.title|tojson|safe }}, |
| content: {{ article.content|tojson|safe }} |
| }; |
| |
| |
| marked.setOptions({ |
| breaks: true, |
| gfm: true |
| }); |
| |
| |
| const chatToggle = document.getElementById('chatToggle'); |
| const chatWindow = document.getElementById('chatWindow'); |
| const chatClose = document.getElementById('chatClose'); |
| const chatInput = document.getElementById('chatInput'); |
| const chatMessages = document.getElementById('chatMessages'); |
| |
| |
| const modelContext = `这是一篇关于"${window.articleContext.title}"的文章。文章内容:\n\n${window.articleContext.content}\n\n请基于以上文章内容来回答用户的问题。`; |
| |
| |
| const welcomeMessage = `您好!我是这篇《${window.articleContext.title}》的AI助手。我已经仔细阅读了全文,可以解答您关于文章内容的任何问题,也提供更深入的讨论和见解,从而帮助您更好地理解文章要点 |
| 让我们开始对话吧!`; |
| |
| |
| let messages = [{ |
| role: 'system', |
| content: modelContext |
| }]; |
| |
| |
| function initializeChat() { |
| displayMessage('assistant', welcomeMessage); |
| } |
| |
| |
| function toggleChat() { |
| chatWindow.classList.toggle('active'); |
| if (chatWindow.classList.contains('active')) { |
| chatToggle.style.display = 'none'; |
| chatInput.focus(); |
| if (chatMessages.children.length === 0) { |
| initializeChat(); |
| } |
| } else { |
| chatToggle.style.display = 'flex'; |
| } |
| } |
| |
| chatToggle.addEventListener('click', toggleChat); |
| chatClose.addEventListener('click', toggleChat); |
| |
| |
| chatInput.addEventListener('input', function() { |
| this.style.height = 'auto'; |
| this.style.height = Math.min(this.scrollHeight, 120) + 'px'; |
| }); |
| |
| |
| async function sendMessage() { |
| const messageText = chatInput.value.trim(); |
| if (!messageText) return; |
| |
| const userMessage = { |
| role: 'user', |
| content: messageText |
| }; |
| |
| |
| chatInput.value = ''; |
| chatInput.style.height = 'auto'; |
| |
| |
| displayMessage('user', messageText); |
| |
| try { |
| const currentMessages = [...messages, userMessage]; |
| |
| const response = await fetch('/api/chat', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ messages: currentMessages }) |
| }); |
| |
| if (response.ok) { |
| const data = await response.json(); |
| |
| |
| messages.push(userMessage); |
| messages.push({ |
| role: 'assistant', |
| content: data.response |
| }); |
| |
| |
| displayMessage('assistant', data.response); |
| } else { |
| throw new Error('Network response was not ok'); |
| } |
| } catch (error) { |
| console.error('Error:', error); |
| displayMessage('assistant', '抱歉,发生了错误,请稍后再试。'); |
| } |
| } |
| |
| |
| function displayMessage(role, content) { |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `chat-message ${role}`; |
| |
| |
| messageDiv.innerHTML = marked.parse(content); |
| |
| chatMessages.appendChild(messageDiv); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| } |
| |
| |
| chatInput.addEventListener('keypress', function(event) { |
| if (event.key === 'Enter' && !event.shiftKey) { |
| event.preventDefault(); |
| sendMessage(); |
| } |
| }); |
| |
| |
| let isDragging = false; |
| let currentX; |
| let currentY; |
| let initialX; |
| let initialY; |
| let xOffset = 0; |
| let yOffset = 0; |
| |
| chatWindow.addEventListener('mousedown', dragStart); |
| document.addEventListener('mousemove', drag); |
| document.addEventListener('mouseup', dragEnd); |
| |
| function dragStart(e) { |
| if (e.target.closest('.chat-header') && !e.target.closest('.chat-close')) { |
| initialX = e.clientX - xOffset; |
| initialY = e.clientY - yOffset; |
| isDragging = true; |
| chatWindow.style.cursor = 'grabbing'; |
| } |
| } |
| |
| function drag(e) { |
| if (isDragging) { |
| e.preventDefault(); |
| currentX = e.clientX - initialX; |
| currentY = e.clientY - initialY; |
| xOffset = currentX; |
| yOffset = currentY; |
| |
| |
| const rect = chatWindow.getBoundingClientRect(); |
| const viewportWidth = window.innerWidth; |
| const viewportHeight = window.innerHeight; |
| |
| |
| if (rect.left < 0) { |
| currentX -= rect.left; |
| } |
| if (rect.right > viewportWidth) { |
| currentX -= (rect.right - viewportWidth); |
| } |
| |
| |
| if (rect.top < 0) { |
| currentY -= rect.top; |
| } |
| if (rect.bottom > viewportHeight) { |
| currentY -= (rect.bottom - viewportHeight); |
| } |
| |
| setTranslate(currentX, currentY, chatWindow); |
| } |
| } |
| |
| function dragEnd() { |
| initialX = currentX; |
| initialY = currentY; |
| isDragging = false; |
| chatWindow.style.cursor = 'default'; |
| } |
| |
| function setTranslate(xPos, yPos, el) { |
| el.style.transform = `translate(${xPos}px, ${yPos}px)`; |
| } |
| </script> |
| {% endblock %} |