mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-19 23:16:48 +00:00
Merge pull request #3091 from qixing-jk/feat-aisummary-wordcount
Feat aisummary wordcount
This commit is contained in:
98
components/AISummary.js
Normal file
98
components/AISummary.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import styles from './AISummary.module.css'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
const AISummary = ({ aiSummary }) => {
|
||||
const { locale } = useGlobal()
|
||||
const [summary, setSummary] = useState(aiSummary)
|
||||
|
||||
useEffect(() => {
|
||||
showAiSummaryAnimation(aiSummary, setSummary)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
aiSummary && (
|
||||
<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']}>{locale.AI_SUMMARY.NAME}</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
|
||||
53
components/AISummary.module.css
Normal file
53
components/AISummary.module.css
Normal 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; }
|
||||
}
|
||||
@@ -1,67 +1,23 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 字数统计
|
||||
* @returns
|
||||
*/
|
||||
export default function WordCount() {
|
||||
export default function WordCount({ wordCount, readTime }) {
|
||||
const { locale } = useGlobal()
|
||||
useEffect(() => {
|
||||
countWords()
|
||||
})
|
||||
|
||||
return <span id='wordCountWrapper' className='flex gap-3 font-light'>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='pl-1 pr-2 fas fa-file-word' />
|
||||
<span id='wordCount'>0</span>
|
||||
</span>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='mr-1 fas fa-clock' />
|
||||
<span></span>
|
||||
<span id='readTime'>0</span> {locale.COMMON.MINUTE}
|
||||
</span>
|
||||
return (
|
||||
<span id='wordCountWrapper' className='flex gap-3 font-light'>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='pl-1 pr-2 fas fa-file-word' />
|
||||
<span>{locale.COMMON.WORD_COUNT}</span>
|
||||
<span id='wordCount'>{wordCount}</span>
|
||||
</span>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='mr-1 fas fa-clock' />
|
||||
<span>{locale.COMMON.READ_TIME}≈</span>
|
||||
<span id='readTime'>{readTime}</span> {locale.COMMON.MINUTE}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字数统计和阅读时间
|
||||
*/
|
||||
function countWords() {
|
||||
const articleText = deleteHtmlTag(document.querySelector('#article-wrapper #notion-article')?.innerHTML)
|
||||
const wordCount = fnGetCpmisWords(articleText)
|
||||
// 阅读速度 300-500每分钟
|
||||
document.getElementById('wordCount').innerHTML = wordCount
|
||||
document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1
|
||||
const wordCountWrapper = document.getElementById('wordCountWrapper')
|
||||
wordCountWrapper.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// 去除html标签
|
||||
function deleteHtmlTag(str) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合
|
||||
return str
|
||||
}
|
||||
|
||||
// 用word方式计算正文字数
|
||||
function fnGetCpmisWords(str) {
|
||||
if (!str) {
|
||||
return 0
|
||||
}
|
||||
let sLen = 0
|
||||
try {
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
str = str.replace(/[\x00-\xff]/g, 'm')
|
||||
str = str.replace(/m+/g, '*')
|
||||
str = str.replace(/龘+/g, '')
|
||||
sLen = str.length
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return sLen
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user