| | {% 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 %} |