From 5eb390bc38ee9748d3da5ed2f9659f354596bfe9 Mon Sep 17 00:00:00 2001 From: anime Date: Mon, 23 Dec 2024 03:01:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=8E=9F=E7=94=9F=E6=94=AF=E6=8C=81AI?= =?UTF-8?q?=E6=91=98=E8=A6=81=E5=8A=9F=E8=83=BD):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=B8=80=E5=B1=82API=E4=BD=9C=E4=B8=BA=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E8=80=8C=E9=9D=9E=E7=9B=B4=E6=8E=A5=E8=AF=B7=E6=B1=82AI?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E4=BB=A5=E5=AE=9E=E7=8E=B0=E7=BC=93=E5=AD=98?= =?UTF-8?q?/=E5=90=8E=E7=AB=AF=E4=BF=9D=E5=AF=86=E5=92=8C=E9=A2=84?= =?UTF-8?q?=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 611a7d1d5dc7bc200d4390e29217ab8c8f76b0f0) --- blog.config.js | 10 ++++ components/AISummary.js | 94 +++++++++++++++++++++++++++++++++ components/AISummary.module.css | 53 +++++++++++++++++++ lib/plugins/aiSummary.js | 31 +++++++++++ pages/[prefix]/[slug]/index.js | 22 ++++++++ themes/heo/index.js | 2 + 6 files changed, 212 insertions(+) create mode 100644 components/AISummary.js create mode 100644 components/AISummary.module.css create mode 100644 lib/plugins/aiSummary.js diff --git a/blog.config.js b/blog.config.js index 3b827a95..161f7715 100644 --- a/blog.config.js +++ b/blog.config.js @@ -270,6 +270,16 @@ 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_WORD_LIMIT: process.env.AI_SUMMARY_WORD_LIMIT || 1000, + + // ********挂件组件相关******** // AI 文章摘要生成 @see https://docs_s.tianli0.top/ TianliGPT_CSS: diff --git a/components/AISummary.js b/components/AISummary.js new file mode 100644 index 00000000..055b4fc5 --- /dev/null +++ b/components/AISummary.js @@ -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 ( +
+
+
+
+ + + +
+
AI智能摘要
+
GPT
+
+
+
+ {summary} + {summary !== aiSummary && ( + + )} +
+
+
+
+ ) +} + +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 diff --git a/components/AISummary.module.css b/components/AISummary.module.css new file mode 100644 index 00000000..b7e89f2a --- /dev/null +++ b/components/AISummary.module.css @@ -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; } +} diff --git a/lib/plugins/aiSummary.js b/lib/plugins/aiSummary.js new file mode 100644 index 00000000..5b9c1d4e --- /dev/null +++ b/lib/plugins/aiSummary.js @@ -0,0 +1,31 @@ +/** + * get Ai summary + * @returns {Promise} + * @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 '获取文章摘要失败,请稍后再试。' + } +} diff --git a/pages/[prefix]/[slug]/index.js b/pages/[prefix]/[slug]/index.js index cdf99afc..6d30e227 100644 --- a/pages/[prefix]/[slug]/index.js +++ b/pages/[prefix]/[slug]/index.js @@ -6,6 +6,8 @@ import { uploadDataToAlgolia } from '@/lib/plugins/algolia' import { checkSlugHasOneSlash, getRecommendPost } from '@/lib/utils/post' import { idToUuid } from 'notion-utils' import Slug from '..' +import { getPageContentText } from '@/pages/search/[keyword]' +import { getAiSummary } from '@/lib/plugins/aiSummary' /** * 根据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 ) 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) diff --git a/themes/heo/index.js b/themes/heo/index.js index 93a7dabb..6fa85552 100644 --- a/themes/heo/index.js +++ b/themes/heo/index.js @@ -42,6 +42,7 @@ import SearchNav from './components/SearchNav' import SideRight from './components/SideRight' import CONFIG from './config' import { Style } from './style' +import AISummary from '@/components/AISummary' /** * 基础布局 采用上中下布局,移动端使用顶部侧边导航栏 @@ -306,6 +307,7 @@ const LayoutSlug = props => {
+ {post && }