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