mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-06-03 07:26:45 +00:00
feat(原生支持AI摘要功能): 使用一层API作为缓存而非直接请求AI,可以实现缓存/后端保密和预渲染
(cherry picked from commit 611a7d1d5dc7bc200d4390e29217ab8c8f76b0f0)
This commit is contained in:
@@ -270,6 +270,16 @@ const BLOG = {
|
|||||||
// 星空雨特效 黑夜模式才会生效
|
// 星空雨特效 黑夜模式才会生效
|
||||||
STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || false, // 开关
|
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_WORD_LIMIT: process.env.AI_SUMMARY_WORD_LIMIT || 1000,
|
||||||
|
|
||||||
|
|
||||||
// ********挂件组件相关********
|
// ********挂件组件相关********
|
||||||
// AI 文章摘要生成 @see https://docs_s.tianli0.top/
|
// AI 文章摘要生成 @see https://docs_s.tianli0.top/
|
||||||
TianliGPT_CSS:
|
TianliGPT_CSS:
|
||||||
|
|||||||
94
components/AISummary.js
Normal file
94
components/AISummary.js
Normal 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
|
||||||
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; }
|
||||||
|
}
|
||||||
31
lib/plugins/aiSummary.js
Normal file
31
lib/plugins/aiSummary.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* get Ai summary
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
* @param aiSummaryAPI
|
||||||
|
* @param aiSummaryKey
|
||||||
|
* @param truncatedText
|
||||||
|
*/
|
||||||
|
export async function getAiSummary(aiSummaryAPI, aiSummaryKey, truncatedText) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(aiSummaryAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: aiSummaryKey,
|
||||||
|
content: truncatedText
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
return data.summary
|
||||||
|
} else {
|
||||||
|
throw new Error('Response not ok')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ChucklePostAI:请求失败', error)
|
||||||
|
return '获取文章摘要失败,请稍后再试。'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
|
|||||||
import { checkSlugHasOneSlash, getRecommendPost } from '@/lib/utils/post'
|
import { checkSlugHasOneSlash, getRecommendPost } from '@/lib/utils/post'
|
||||||
import { idToUuid } from 'notion-utils'
|
import { idToUuid } from 'notion-utils'
|
||||||
import Slug from '..'
|
import Slug from '..'
|
||||||
|
import { getPageContentText } from '@/pages/search/[keyword]'
|
||||||
|
import { getAiSummary } from '@/lib/plugins/aiSummary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据notion的slug访问页面
|
* 根据notion的slug访问页面
|
||||||
@@ -94,6 +96,26 @@ export async function getStaticProps({ params: { prefix, slug }, locale }) {
|
|||||||
key => props.post.blockMap.block[key]?.value?.parent_id === props.post.id
|
key => props.post.blockMap.block[key]?.value?.parent_id === props.post.id
|
||||||
)
|
)
|
||||||
props.post.toc = getPageTableOfContents(props.post, props.post.blockMap)
|
props.post.toc = getPageTableOfContents(props.post, props.post.blockMap)
|
||||||
|
|
||||||
|
const aiSummaryAPI = siteConfig('AI_SUMMARY_API')
|
||||||
|
if (aiSummaryAPI) {
|
||||||
|
const aiSummaryKey = siteConfig('AI_SUMMARY_KEY')
|
||||||
|
const wordLimit = siteConfig('AI_SUMMARY_WORD_LIMIT', '1000')
|
||||||
|
const post = props.post
|
||||||
|
let content = ''
|
||||||
|
for (let heading of post.toc) {
|
||||||
|
content += heading.text + ' '
|
||||||
|
}
|
||||||
|
content += getPageContentText(post, post.blockMap)
|
||||||
|
const combinedText = post.title + ' ' + content
|
||||||
|
const truncatedText = combinedText.slice(0, wordLimit)
|
||||||
|
|
||||||
|
props.post.aiSummary = await getAiSummary(
|
||||||
|
aiSummaryAPI,
|
||||||
|
aiSummaryKey,
|
||||||
|
truncatedText
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
|
// 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import SearchNav from './components/SearchNav'
|
|||||||
import SideRight from './components/SideRight'
|
import SideRight from './components/SideRight'
|
||||||
import CONFIG from './config'
|
import CONFIG from './config'
|
||||||
import { Style } from './style'
|
import { Style } from './style'
|
||||||
|
import AISummary from '@/components/AISummary'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础布局 采用上中下布局,移动端使用顶部侧边导航栏
|
* 基础布局 采用上中下布局,移动端使用顶部侧边导航栏
|
||||||
@@ -306,6 +307,7 @@ const LayoutSlug = props => {
|
|||||||
<section
|
<section
|
||||||
className='wow fadeInUp p-5 justify-center mx-auto'
|
className='wow fadeInUp p-5 justify-center mx-auto'
|
||||||
data-wow-delay='.2s'>
|
data-wow-delay='.2s'>
|
||||||
|
<AISummary aiSummary={post.aiSummary}/>
|
||||||
<WWAds orientation='horizontal' className='w-full' />
|
<WWAds orientation='horizontal' className='w-full' />
|
||||||
{post && <NotionPage post={post} />}
|
{post && <NotionPage post={post} />}
|
||||||
<WWAds orientation='horizontal' className='w-full' />
|
<WWAds orientation='horizontal' className='w-full' />
|
||||||
|
|||||||
Reference in New Issue
Block a user