Skip to content

Commit

Permalink
Merge pull request #3091 from qixing-jk/feat-aisummary-wordcount
Browse files Browse the repository at this point in the history
Feat aisummary wordcount
  • Loading branch information
tangly1024 authored Jan 1, 2025
2 parents cfd690c + b84f16a commit d71cf3e
Show file tree
Hide file tree
Showing 26 changed files with 418 additions and 395 deletions.
11 changes: 11 additions & 0 deletions blog.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ const BLOG = {
// 星空雨特效 黑夜模式才会生效
STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || false, // 开关

// AI 文章摘要生成
AI_SUMMARY_API:
process.env.AI_SUMMARY_API||
'',
AI_SUMMARY_KEY:
process.env.AI_SUMMARY_KEY ||
'',
AI_SUMMARY_CACHE_TIME: process.env.AI_SUMMARY_CACHE_TIME || 1800, // 缓存时间,单位秒
AI_SUMMARY_WORD_LIMIT: process.env.AI_SUMMARY_WORD_LIMIT || 1000,


// ********挂件组件相关********
// AI 文章摘要生成 @see https://docs_s.tianli0.top/
TianliGPT_CSS:
Expand Down
98 changes: 98 additions & 0 deletions components/AISummary.js
Original file line number Diff line number Diff line change
@@ -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 changes: 53 additions & 0 deletions components/AISummary.module.css
Original file line number Diff line number Diff line change
@@ -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; }
}
74 changes: 15 additions & 59 deletions components/WordCount.js
Original file line number Diff line number Diff line change
@@ -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>&nbsp;{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>&nbsp;
<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>&nbsp;
<span id='readTime'>{readTime}</span>&nbsp;{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标签和&nbsp;之类的特殊符合
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
}
)
}
4 changes: 2 additions & 2 deletions lib/cache/cache_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ export async function getDataFromCache(key, force) {
}
}

export async function setDataToCache(key, data) {
export async function setDataToCache(key, data, customCacheTime) {
if (!data) {
return
}
// console.trace('[API-->>缓存写入]:', key)
await getApi().setCache(key, data)
await getApi().setCache(key, data, customCacheTime)
}

export async function delCacheData(key) {
Expand Down
4 changes: 2 additions & 2 deletions lib/cache/memory_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export async function getCache(key, options) {
return await cache.get(key)
}

export async function setCache(key, data) {
await cache.put(key, data, cacheTime * 1000)
export async function setCache(key, data, customCacheTime) {
await cache.put(key, data, (customCacheTime || cacheTime) * 1000)
}

export async function delCache(key) {
Expand Down
7 changes: 4 additions & 3 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
case 'TAG_SORT_BY_COUNT':
case 'THEME':
case 'LINK':
case 'NPM_CDN_BASE':
case 'CDNJS_CDN_BASE':
case 'JSDELIVR_CDN_BASE':
case 'AI_SUMMARY_API':
case 'AI_SUMMARY_KEY':
case 'AI_SUMMARY_CACHE_TIME':
case 'AI_SUMMARY_WORD_LIMIT':
// LINK比较特殊,
if (key === 'LINK') {
if (!extendConfig || Object.keys(extendConfig).length === 0) {
Expand Down
3 changes: 3 additions & 0 deletions lib/lang/en-US.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,8 @@ export default {
SUBSCRIBE: 'Subscribe',
MSG: 'Get the latest news and articles to your inbox every month.',
EMAIL: 'Email'
},
AI_SUMMARY: {
NAME: 'AI intelligent summary',
}
}
8 changes: 6 additions & 2 deletions lib/lang/fr-FR.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ export default {
URL_COPIED: "L'URL est copé!",
TABLE_OF_CONTENTS: 'Sommaire',
RELATE_POSTS: 'Article similaire',
COPYRIGHT: 'Droit d\'auteur',
COPYRIGHT: "Droit d'auteur",
AUTHOR: 'Auteur',
URL: 'Link',
ANALYTICS: 'Analytique',
POSTS: 'Articles',
ARTICLE: 'Article(s)',
VISITORS: 'Visiteurs',
VIEWS: 'Views',
COPYRIGHT_NOTICE: 'Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International (CC BY-NC-SA 4.0)',
COPYRIGHT_NOTICE:
'Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International (CC BY-NC-SA 4.0)',
RESULT_OF_SEARCH: 'Résultats',
ARTICLE_DETAIL: 'Plus de détails',
PASSWORD_ERROR: 'Mot de passe est incorrect!',
Expand All @@ -50,5 +51,8 @@ export default {
POST: {
BACK: 'Page precedente',
TOP: 'Haut'
},
AI_SUMMARY: {
NAME: "Résumé intelligent par l'IA",
}
}
3 changes: 3 additions & 0 deletions lib/lang/ja-JP.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,8 @@ export default {
POST: {
BACK: '前のページに戻る',
TOP: '上に戻る'
},
AI_SUMMARY: {
NAME: 'AIインテリジェントサマリー',
}
}
3 changes: 3 additions & 0 deletions lib/lang/tr-TR.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,8 @@ export default {
POST: {
BACK: 'Geri',
TOP: 'Yukarı'
},
AI_SUMMARY: {
NAME: 'Yapay Zeka Akıllı Özet',
}
}
3 changes: 3 additions & 0 deletions lib/lang/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,8 @@ export default {
SUBSCRIBE: '邮件订阅',
MSG: '订阅以获取每月更新的新闻和文章,直接发送至您的邮箱。',
EMAIL: '邮箱'
},
AI_SUMMARY: {
NAME: 'AI智能摘要',
}
}
3 changes: 3 additions & 0 deletions lib/lang/zh-HK.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@ export default {
POST: {
BACK: '返回',
TOP: '回到頂端'
},
AI_SUMMARY: {
NAME: 'AI智能摘要',
}
}
3 changes: 3 additions & 0 deletions lib/lang/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@ export default {
POST: {
BACK: '返回',
TOP: '回到頂端'
},
AI_SUMMARY: {
NAME: 'AI智能摘要',
}
}
Loading

0 comments on commit d71cf3e

Please sign in to comment.