feat(原生支持AI摘要功能): 使用一层API作为缓存而非直接请求AI,可以实现缓存/后端保密和预渲染

(cherry picked from commit 611a7d1d5dc7bc200d4390e29217ab8c8f76b0f0)
This commit is contained in:
anime
2024-12-23 03:01:35 +08:00
parent b70c0f4ee7
commit 5eb390bc38
6 changed files with 212 additions and 0 deletions

94
components/AISummary.js Normal file
View File

@@ -0,0 +1,94 @@
import styles from './AISummary.module.css'
import { useEffect, useState } from 'react'
const AISummary = ({ aiSummary }) => {
const [summary, setSummary] = useState('生成中...')
useEffect(() => {
showAiSummaryAnimation(aiSummary, setSummary)
}, [])
return (
<div className={styles['post-ai']}>
<div className={styles['ai-container']}>
<div className={styles['ai-header']}>
<div className={styles['ai-icon']}>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 24 24'
width='24'
height='24'>
<path
fill='#ffffff'
d='M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8Z'
/>
</svg>
</div>
<div className={styles['ai-title']}>AI智能摘要</div>
<div className={styles['ai-tag']}>GPT</div>
</div>
<div className={styles['ai-content']}>
<div className={styles['ai-explanation']}>
{summary}
{summary !== aiSummary && (
<span className={styles['blinking-cursor']}></span>
)}
</div>
</div>
</div>
</div>
)
}
const showAiSummaryAnimation = (rawSummary, setSummary) => {
if (!rawSummary) return
let currentIndex = 0
const typingDelay = 20
const punctuationDelayMultiplier = 6
let animationRunning = true
let lastUpdateTime = performance.now()
const animate = () => {
if (currentIndex < rawSummary.length && animationRunning) {
const currentTime = performance.now()
const timeDiff = currentTime - lastUpdateTime
const letter = rawSummary.slice(currentIndex, currentIndex + 1)
const isPunctuation = /[,。!、?,.!?]/.test(letter)
const delay = isPunctuation
? typingDelay * punctuationDelayMultiplier
: typingDelay
if (timeDiff >= delay) {
setSummary(rawSummary.slice(0, currentIndex + 1))
lastUpdateTime = currentTime
currentIndex++
if (currentIndex < rawSummary.length) {
setSummary(rawSummary.slice(0, currentIndex))
} else {
setSummary(rawSummary)
observer.disconnect()
}
}
requestAnimationFrame(animate)
}
}
animate(rawSummary)
const observer = new IntersectionObserver(
entries => {
animationRunning = entries[0].isIntersecting
if (animationRunning && currentIndex === 0) {
setTimeout(() => {
requestAnimationFrame(animate)
}, 200)
}
},
{ threshold: 0 }
)
let post_ai = document.querySelector('.post-ai')
if (post_ai) {
observer.observe(post_ai)
}
}
export default AISummary

View File

@@ -0,0 +1,53 @@
.post-ai {
font-family: 'Noto Sans SC', sans-serif;
margin-bottom: 20px;
}
.ai-container {
background: linear-gradient(135deg, #f9f9f9 0%, #f5f5f5 100%);
border: 1px solid #e8e8e8;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.ai-header {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
padding: 12px 20px;
display: flex;
align-items: center;
}
.ai-icon {
margin-right: 10px;
}
.ai-title {
font-size: 18px;
font-weight: bold;
flex-grow: 1;
}
.ai-tag {
background-color: rgba(255, 255, 255, 0.2);
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
}
.ai-content {
padding: 20px;
}
.ai-explanation {
font-size: 16px;
line-height: 1.6;
color: #333;
}
.blinking-cursor {
display: inline-block;
width: 2px;
height: 20px;
background-color: #333;
animation: blink 0.7s infinite;
margin-left: 5px;
}
@keyframes blink {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}