前言
@Heo又整了新活,给文章加上了AI,我也开写,竞速。
项目地址Post-Summary-AI
大半夜领先一点就睡不着的屑Heo
有这些特性:
- 真AI!,接入tianliGPT,JS动态获取全文所有纯文本,传给api实时获取文章摘要(受限于tianliGPT的成本,目前仍有缓存机制)
- 基于tianliGPT标注关键词、Python分析相关度的相关文章AI推荐
- 遇到标点符号慢下来,动态打字速度
- 使用requestAnimationFrame优化性能,动态清除setTimeout、打断fetch请求,交互放心
- 使用IntersectionObserver监听,当容器在视口不可见后停止生成摘要,重新出现在视口后继续生成
- 模拟GPT的光标效果
- 多个按钮实现多个功能,自我介绍、文章摘要、推荐相关文章等
- 良好兼容性,性能消耗小。
- 适配pjax
- 简单引入js后,即可生成QX-AI并自动挂载初始化
- 配置项多样,高度自定义
- 支持切换摘要、摘要语音朗读
未来的计划:
- 目前数据库文章量已经够多,计划构建一个博文社区
5.0积累更新
待 5. 版本稳定后发布 6.0 版本*
项目地址Post-Summary-AI
更多详情请看项目仓库README
新特性:
- 优化文章内容的获取
- 新增适配pjax的配置项
- 自定义css配置项
- 添加svg图标
- 新增文章截取相关配置项
- 更新CSS,优化UI样式
- 新增注入额外CSS配置项
- 新增切换简介功能及相关配置
- 新增摘要语音朗读功能
- 新增UI文字自定义配置
- 新增页面排除配置项
- 标识访客唯一ID
- 新增直接显示摘要的配置项
- 优化JS的业务逻辑,精简代码
- 新增自动挂载:自动获取文章内容所在容器元素,而无需el配置项
- 新增白名单:只让指定页面显示摘要AI
- js动态插入css,无需额外引入
- 新增控制打字机效果的配置项,包括:是否启用打字机效果、打字速度
- 新增矩阵穿梭功能及按钮,矩阵穿梭:随机前往一个部署了AI摘要的网站
- 新增是否启用矩阵穿梭功能的配置项,默认开启
4.0重磅更新
新特性:
- 新增相关文章AI推荐
- 引入缓存机制,减轻后端压力
- 优化代码及性能
快速上手
项目地址Post-Summary-AI
非常简单,引入下面这些代码到你的网站内,并修改配置项后即可
TIP: 为避免CDN和浏览器缓存的影响,建议指定资源版本号使用
<!-- css --><link rel="stylesheet" href="https://cdn1.tianli0.top/gh/qxchuckle/Post-Summary-AI/chuckle-post-ai.css"><!-- chuckle-post-ai.js现在可以在网页结构的任何位置插入,只要你能够 --><script src="https://cdn1.tianli0.top/gh/qxchuckle/Post-Summary-AI/chuckle-post-ai.js"></script><!-- 但要确保的是,AI构造代码一定要在chuckle-post-ai.js之后插入 --><script data-pjax defer> // AI构造函数 new ChucklePostAI({ /* 必须配置 */ // 文章内容所在的元素属性的选择器,也是AI挂载的容器,AI将会挂载到该容器的最前面 el: '#post>#article-container', // 驱动AI所必须的key,即是tianliGPT后端服务所必须的key key:'123456', /* 非必须配置 */ // 文章标题所在的元素属性的选择器,默认获取当前网页的标题 title_el: '.post-title', // 文章推荐方式,all:匹配数据库内所有文章进行推荐,web:仅当前站内的文章,默认all rec_method: 'web', // 获取文章内容时,需要排除的元素及其子元素,默认如下 exclude: ['highlight', 'Copyright-Notice', 'post-ai', 'post-series', 'mini-sandbox'] })</script>AI构造函数 ChucklePostAI({ /* 传入配置对象 */ }) 详解
el文章内容所在的元素属性的选择器,也是AI挂载的容器,AI将会挂载到该容器的最前面key驱动AI所必须的key,即是tianliGPT后端服务所必须的keytitle_el文章标题所在的元素属性的选择器,默认获取当前网页的标题rec_method文章推荐方式,all:匹配数据库内所有文章进行推荐,web:仅当前站内的文章,默认allexclude获取文章内容时,需要排除的元素及其子元素
tianliGPT的key请到爱发电中购买,10元5万字符(常有优惠)。请求过的内容再次请求不会消耗key,可以无限期使用。
更多详情请看项目仓库README
3.0全新版本
新特性:
- 简单引入js后,即可生成QX-AI并自动挂载初始化
- 新增项目地址Post-Summary-AI
- 优化代码逻辑,引入外部配置
快速上手
非常简单,引入下面这些代码到你的网站内即可,其中JS的引入的位置应该在文章内容之后
<link rel="stylesheet" href="https://cdn1.tianli0.top/gh/qxchuckle/Post-Summary-AI/chuckle-post-ai.css"><script data-pjax defer="true"> var ai_option = { el: '#post #article-container', key:'123456' }</script><script src="https://cdn1.tianli0.top/gh/qxchuckle/Post-Summary-AI/chuckle-post-ai.js" data-pjax defer="true"></script>ai_option配置详解:
- el 文章内容所在的元素属性的选择器,也是AI挂载的容器,AI将会挂载到该容器的最前面
- key 驱动AI所必须的key,即是tianliGPT后端服务所必须的key
更多详情请看项目仓库README
2.0之前旧版配置
注意: 1、为确保 recommendList() 函数正常运行、相关推荐能正常生成,btf主题用户请打开文章页侧边栏中的两个板块:最新文章和相关推荐,非btf用户也许需要手动更改相关js 2、tianliGPT中国大陆访问速度快,key与域名相绑定,无需担心盗刷,一次生成后续不消耗key字数,tianliGPT的key购买渠道:购买tianliGPT-Key 3、如有bug,请在评论区讨论,请忽略js中的不规范命名和一堆变量,会优化的咕咕咕。
手动修改js中这部分常量以使用AI实时简介
//默认true,使用tianliGPT,false使用官方api,记得配置Keyconst choiceApi = true;const apiKey = "填入chatGPT的apiKey";//tianliGPT的参数const tlReferer = 'https://你的授权域名/';const tlKey = '填入tianliGPT的key';虽然以及有AI实时生成了,但还是需要预设的,在markdown文件的matter添加ai配置,放入ai事先生成好的文章简介或提升去点AI生成按钮,不想出现AI模块的文章就删去这个配置项
---title:ai: 暂无预设简介,请点击下方生成AI简介按钮。---2.0版
新特性:
- 真!AI,接入tianliGPT或是使用官方api接口,随意选择
- 前端限制请求频率、动态打断fetch
- 压缩文本降低key压力,纯文本1000字以上的文章,截取前500后200中间300字生成摘要,降低key字数消耗,当然,可以给 getTextContent 传入第二个参数为false,从而不压缩文本
修改post.pug,将下面代码加在合适的位置,如 article#article-container.post-content 后,注意缩进
if page.ai .post-ai .ai-title i.fa-brands.fa-airbnb .ai-title-text QX-AI .ai-tag GPT-4 .ai-explanation QX-AI初始化中... .ai-explanation-none #{page.ai} .ai-btn-box .ai-btn-item 介绍自己 .ai-btn-item 生成本文简介 .ai-btn-item 推荐相关文章 .ai-btn-item 前往主页 script(src="/js/post-ai.js" defer="true" data-pjax)新建post-ai.js
4-16修复了若干bug
function allAI() { let animationRunning = true; // 标志变量,控制动画函数的运行 let explanation = document.querySelector('.ai-explanation'); let abstract_value = document.querySelector('.ai-explanation-none').innerHTML; let post_ai = document.querySelector('.post-ai'); let ai_btn_item = document.querySelectorAll('.ai-btn-item'); let ai_str = ''; let ai_str_length = ''; let delay_init = 600; let i = 0; let j = 0; let sto = []; let elapsed = 0; let completeGenerate = false; let controller = new AbortController();//控制fetch let signal = controller.signal; //默认true,使用tianliGPT,false使用官方api,记得配置Key const choiceApi = true; const apiKey = "填入chatGPT的apiKey"; //tianliGPT的参数 const tlReferer = 'https://你的授权域名/'; const tlKey = '填入tianliGPT的key'; //----------------------------------------------- const animate = (timestamp) => { if (!animationRunning) { return; // 动画函数停止运行 } if (!animate.start) animate.start = timestamp; elapsed = timestamp - animate.start; if (elapsed >= 20) { animate.start = timestamp; if (i < ai_str_length - 1) { let char = ai_str.charAt(i + 1); let delay = /[,.,。!?!?]/.test(char) ? 150 : 20; if (explanation.firstElementChild) { explanation.removeChild(explanation.firstElementChild); } explanation.innerHTML += char; let div = document.createElement('div'); div.className = "ai-cursor"; explanation.appendChild(div); i++; if (delay === 150) { document.querySelector('.ai-explanation .ai-cursor').style.opacity = "0"; } if (i === ai_str_length - 1) { observer.disconnect();// 暂停监听 explanation.removeChild(explanation.firstElementChild); } sto[0] = setTimeout(() => { requestAnimationFrame(animate); }, delay); } } else { requestAnimationFrame(animate); } }; const observer = new IntersectionObserver((entries) => { let isVisible = entries[0].isIntersecting; animationRunning = isVisible; // 标志变量更新 if (animationRunning) { delay_init = i === 0 ? 200 : 20; sto[1] = setTimeout(() => { if (j) { i = 0; j = 0; } if (i === 0) { explanation.innerHTML = ai_str.charAt(0); } requestAnimationFrame(animate); }, delay_init); } }, { threshold: 0 }); function clearSTO() { if (sto.length) { sto.forEach((item) => { if (item) { clearTimeout(item); } }); } } function resetAI(df = true) { i = 0;//重置计数器 j = 1; clearSTO(); animationRunning = false; elapsed = 0; if (df) { explanation.innerHTML = '生成中. . .'; } else { explanation.innerHTML = '请等待. . .'; } // console.log(completeGenerate); if (!completeGenerate) { controller.abort(); // console.log(completeGenerate); } ai_str = ''; ai_str_length = ''; observer.disconnect();// 暂停上一次监听 } function startAI(str, df = true) { resetAI(df); ai_str = str; ai_str_length = ai_str.length; observer.observe(post_ai);//启动新监听 } function aiIntroduce() { startAI('我是文章辅助AI: QX-AI,点击下方的按钮,让我生成本文简介、推荐相关文章等。'); } function aiAbstract() { startAI(abstract_value); } function aiRecommend() { resetAI(); sto[2] = setTimeout(() => { explanation.innerHTML = recommendList(); }, 300); } function aiGoHome() { startAI('正在前往博客主页...', false); sto[2] = setTimeout(() => { pjax.loadUrl("/"); }, 1000); } async function aiGenerateAbstract() { // if(!verifyDomainName()){btf.snackbarShow('未经授权的域名');return;} // if(!completeGenerate){ // btf.snackbarShow('AI摘要正在生成,请勿重复发起'); // return; // } if (clickFrequency()) { return; } localStorage.setItem('aiTime', Date.now()); resetAI(); const ele = document.querySelector('#article-container'); const content = getTextContent(ele); console.log(content); const response = await getGptResponse(content, choiceApi);//true使用tianliGPT,false使用官方api console.log(response); startAI(response); } function recommendList() { let info = `推荐文章:<br />`; let thumbnail = document.querySelectorAll('.card-recommend-post .aside-list .aside-list-item .thumbnail'); if (!thumbnail||thumbnail.length===0) { info = '很抱歉,无法找到类似的文章,你也可以看看本站最近更新的文章:<br />'; thumbnail = document.querySelectorAll('.card-recent-post .aside-list .aside-list-item .thumbnail'); } info += '<div class="ai-recommend">'; thumbnail.forEach((item, index) => { info += `<div class="ai-recommend-item"><span>推荐${index + 1}:</span><a href="javascript:;" onclick="pjax.loadUrl('${item.href}')" title="${item.title}" data-pjax-state="">${item.title}</a></div>`; }); info += '</div>' return info; } //ai首屏初始化,绑定按钮注册事件 function ai_init() { explanation = document.querySelector('.ai-explanation'); abstract_value = document.querySelector('.ai-explanation-none').innerHTML; post_ai = document.querySelector('.post-ai'); ai_btn_item = document.querySelectorAll('.ai-btn-item'); const funArr = [aiIntroduce, aiAbstract, aiRecommend, aiGenerateAbstract]; ai_btn_item.forEach((item, index) => { item.addEventListener('click', () => { funArr[index](); }); }); aiIntroduce(); } function clickFrequency(t = 3000) { let time = Date.now() - localStorage.getItem('aiTime'); if (time < t) { btf.snackbarShow(`${3 - parseInt(time / 1000)}后才能再次点击真AI简介`); return true; } else { return false; } } // 真AI简介相关函数
// function verifyDomainName(){ // const domain = window.location.hostname; // const authorized = ['www.qcqx.cn','www.chuckle.top','127.0.0.1']; // return authorized.includes(domain) // } //获取某个元素内的所有纯文本,并按顺序拼接返回 function getText(element) { //需要排除的元素及其子元素 const excludeClasses = ['highlight', 'Copyright-Notice', 'post-ai', 'post-series', 'mini-sandbox',]; let textContent = ''; for (let node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { //如果是纯文本节点则获取内容拼接 textContent += node.textContent.trim(); } else if (node.nodeType === Node.ELEMENT_NODE) { let hasExcludeClass = false;//跟踪元素是否包含需要排除的类名 //遍历类名 for (let className of node.classList) { //如果包含则hasExcludeClass设为true,且break跳出循环 if (excludeClasses.includes(className)) { hasExcludeClass = true; break; } } //如果hasExcludeClass为false,即该标签不包含需要排除的类,可以继续向下遍历子元素 if (!hasExcludeClass) { //不同元素内获取的文本用句号隔开 let innerTextContent = getText(node); // if (textContent && innerTextContent) { // //如果本来有标点符号则不添加 // if (/[::,.,。??/;;!!()、)(]$/.test(textContent) || /^[::,.,。??/;;!!()、@#¥$%&)(]/.test(innerTextContent)) { // textContent += innerTextContent; // } else { // textContent += '。' + innerTextContent; // } // } else { // textContent += innerTextContent; // } textContent += innerTextContent; } } } //返回纯文本节点的内容 return textContent.replace(/\s+/g, ''); } //获取各级标题 function extractHeadings(element) { const headings = element.querySelectorAll('h1, h2, h3, h4'); const result = []; for (let i = 0; i < headings.length; i++) { const heading = headings[i]; const headingText = heading.textContent.trim(); result.push(headingText); const childHeadings = extractHeadings(heading); result.push(...childHeadings); } return result.join(";"); } //按比例切割字符串 function extractString(str) { // 截取前500个字符 var first500 = str.substring(0, 500); // 截取末尾200个字符 var last200 = str.substring(str.length - 200); // 截取中间300个字符 var midStartIndex = (str.length - 300) / 2; // 计算中间部分的起始索引 var middle300 = str.substring(midStartIndex, midStartIndex + 300); // 将三个部分拼接在一起 var result = first500 + middle300 + last200; // 返回截取后的字符串 return result; } //获得字符串,默认进行切割,false返回原文纯文本 function getTextContent(element, i = true) { let content; if (i) { content = `文章的各级标题:${extractHeadings(element)}。文章内容的截取:${extractString(getText(element))}`; } else { content = `${getText(element)}`; } return content; } //发送请求获得简介 async function getGptResponse(content, i = true) { completeGenerate = false; controller = new AbortController(); signal = controller.signal; let response = ''; if (i) { try { response = await fetch('https://summary.tianli0.top/', { signal: signal, method: "POST", headers: { "Content-Type": "application/json", "Referer": tlReferer }, body: JSON.stringify({ content: content, key: tlKey }) }); if (response.status === 429) { startAI('请求过于频繁,请稍后再请求AI。'); } if (!response.ok) { throw new Error('Response not ok'); } // 处理响应 } catch (error) { console.error('Error occurred:', error); startAI("QX-AI请求tianliGPT出错了,请稍后再试。"); } // 解析响应并返回结果 const data = await response.json(); const outputText = data.summary; // console.log(outputText); completeGenerate = true; return outputText; } else { const prompt = `你是一个摘要生成工具,你需要解释我发送给你的内容,不要换行,不要超过200字,只需要介绍文章的内容,不需要提出建议和缺少的东西。请用中文回答,文章内容为:${content}`; const apiUrl = "https://api.openai.com/v1/chat/completions"; try { response = await fetch(apiUrl, { signal: signal, method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, body: JSON.stringify({ model: "gpt-3.5-turbo", messages: [{ "role": "user", "content": prompt }], }) }); if (response.status === 429) { startAI('请求过于频繁,请稍后再请求AI。'); } if (!response.ok) { throw new Error('Response not ok'); } // 处理响应 } catch (error) { console.error('Error occurred:', error); startAI("QX-AI请求chatGPT出错了,请稍后再试。"); } // 解析响应并返回结果 const data = await response.json(); const outputText = data.choices[0].message.content; completeGenerate = true; return outputText; } } ai_init();}allAI();添加CSS,颜色变量可F12自取,建议自己重新配色(2.0版本无css更新)
.post-ai{ background: var(--ai-post-bg); border-radius: 12px; padding: 12px 16px; line-height: 1.3; border: var(--ai-border); margin-top: 10px; margin-bottom: 6px;}.ai-title{ display: flex; color: var(--font-color); border-radius: 8px; align-items: center; padding: 0 5px;}.ai-title i{ font-weight: 800;}.ai-title-text{ font-weight: bold; margin-left: 8px;}.ai-tag{ font-size: 12px; background-color: var(--ai-tag-bg); color: rgba(255,255,255,0.9); border-radius: 4px; margin-left: auto; line-height: 1; padding: 4px 5px;}.ai-explanation{ margin-top: 11px; font-size: 15.5px; line-height: 1.4;}.ai-cursor{ display: inline-block; width: 7px; background: #333; height: 16px; margin-bottom: -2px; opacity: 0.95; margin-left: 3px; transition: all 0.3s; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s;}[data-theme=dark].ai-cursor{ background: rgb(255, 255, 255, 0.9);}.ai-btn-box{ font-size: 15.5px; width: 100%; display: flex; flex-direction: row; flex-wrap: wrap;}.ai-btn-item{ padding: 5px 10px; margin: 10px 16px 0px 5px; width: fit-content; line-height: 1; background: rgba(48, 52, 63, 0.75); color: #fff; border-radius: 6px 6px 6px 0; -webkit-border-radius: 6px 6px 6px 0; -moz-border-radius: 6px 6px 6px 0; -ms-border-radius: 6px 6px 6px 0; -o-border-radius: 6px 6px 6px 0; user-select: none; transition: all 0.3s; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s;}.ai-btn-item:hover{ background: #49b0f5dc;}.ai-recommend{ display: flex; flex-direction: row; flex-wrap: wrap;}.ai-recommend-item{ width: 50%; margin-top: 2px;}@media screen and (max-width:768px){ .ai-btn-box{ justify-content: center; } .ai-recommend .ai-recommend-item{ width: 100%; }}.ai-explanation-none{ position: absolute; opacity: 0; width: 0; height: 0; z-index: -999;}1.1版
修改post.pug,将下面代码加在合适的位置,如 article#article-container.post-content 后,注意缩进
if page.ai .post-ai .ai-title i.fa-brands.fa-airbnb .ai-title-text QX-AI .ai-tag GPT-4 .ai-explanation QX-AI初始化中... .ai-explanation-none #{page.ai} .ai-btn-box .ai-btn-item 介绍自己 .ai-btn-item 生成本文简介 .ai-btn-item 推荐相关文章 .ai-btn-item 前往主页 script(src="/js/post-ai.js?2" defer="true" data-pjax)新建post-ai.js
if (true) { let animationRunning = true; // 标志变量,控制动画函数的运行 const explanation = document.querySelector('.ai-explanation'); const abstract_value = document.querySelector('.ai-explanation-none').innerHTML; const post_ai = document.querySelector('.post-ai'); let ai_str = ''; let ai_str_length = ''; let delay_init = 600; let i = 0; let j = 0; let sto = []; let elapsed = 0; const animate = (timestamp) => { if (!animationRunning) { return; // 动画函数停止运行 } if (!animate.start) animate.start = timestamp; elapsed = timestamp - animate.start; if (elapsed >= 20) { animate.start = timestamp; if (i < ai_str_length-1) { let char = ai_str.charAt(i+1); let delay = /[,.,。!?!?]/.test(char) ? 150 : 20; if(explanation.firstElementChild){ explanation.removeChild(explanation.firstElementChild); } explanation.innerHTML += char; let div = document.createElement('div'); div.className = "ai-cursor"; explanation.appendChild(div); i++; if(delay === 150){ document.querySelector('.ai-explanation .ai-cursor').style.opacity = "0"; } if(i === ai_str_length-1){ observer.disconnect();// 暂停监听 explanation.removeChild(explanation.firstElementChild); } sto[0] = setTimeout(() => { requestAnimationFrame(animate); }, delay); } } else { requestAnimationFrame(animate); } }; const observer = new IntersectionObserver((entries) => { let isVisible = entries[0].isIntersecting; animationRunning = isVisible; // 标志变量更新 if(animationRunning){ delay_init = i===0 ? 200 : 20; sto[1] = setTimeout(() => { if(j){ i = 0; j = 0; } if(i===0){ explanation.innerHTML = ai_str.charAt(0); } requestAnimationFrame(animate); }, delay_init); } }, { threshold: 0 }); function clearSTO(){ if(sto.length){ sto.forEach((item)=>{ if(item){ clearTimeout(item); } }); } } function startAI(str,df=true){ i = 0;//重置计数器 j = 1; clearSTO(); animationRunning = false; elapsed = 0; if(df){ explanation.innerHTML = '生成中. . .'; }else{ explanation.innerHTML = '请等待. . .'; } ai_str = str; ai_str_length = ai_str.length; observer.disconnect();// 暂停上一次监听 observer.observe(post_ai);//启动新监听 } function aiIntroduce(){ startAI('我是文章辅助AI: QX-AI,点击下方的按钮,让我生成本文简介、推荐相关文章等。'); } function aiAbstract(){ startAI(abstract_value); } function aiRecommend(){ i = 0;//重置计数器 j = 1; clearSTO(); animationRunning = false; elapsed = 0; explanation.innerHTML = '生成中. . .'; ai_str = ''; ai_str_length = ''; observer.disconnect();// 暂停上一次监听 sto[2] = setTimeout(() => { explanation.innerHTML = recommendList(); }, 600); } function aiGoHome(){ startAI('正在前往博客主页...',false); sto[2] = setTimeout(() => { pjax.loadUrl("/"); }, 1000); } const ai_btn_item = document.querySelectorAll('.ai-btn-item'); ai_btn_item.forEach((item, index)=>{ item.addEventListener('click', ()=>{ switch(index) { case 0: aiIntroduce(); break; case 1: aiAbstract(); break; case 2: aiRecommend(); break; case 3: aiGoHome(); break; } }); }); function recommendList(){ let info = `推荐文章:<br />`; let thumbnail = document.querySelectorAll('.card-recommend-post .aside-list .aside-list-item .thumbnail'); if(!thumbnail){ info = '很抱歉,无法找到类似的文章,你也可以看看本站最新发布的文章:<br />'; thumbnail = document.querySelectorAll('.card-recent-post .aside-list .aside-list-item .thumbnail'); } info += '<div class="ai-recommend">'; thumbnail.forEach((item, index)=>{ info += `<div class="ai-recommend-item"><span>推荐${index+1}:</span><a href="javascript:;" onclick="pjax.loadUrl('${item.href}')" title="${item.title}" data-pjax-state="">${item.title}</a></div>`; }); info += '</div>' return info; } aiIntroduce();}添加CSS,颜色变量可F12自取
.post-ai{ background: var(--ai-post-bg); border-radius: 12px; padding: 12px 16px; line-height: 1.3; border: var(--ai-border); margin-top: 10px; margin-bottom: 6px;}.ai-title{ display: flex; color: var(--font-color); border-radius: 8px; align-items: center; padding: 0 5px;}.ai-title i{ font-weight: 800;}.ai-title-text{ font-weight: bold; margin-left: 8px;}.ai-tag{ font-size: 12px; background-color: var(--ai-tag-bg); color: rgba(255,255,255,0.9); border-radius: 4px; margin-left: auto; line-height: 1; padding: 4px 5px;}.ai-explanation{ margin-top: 11px; font-size: 15.5px; line-height: 1.4;}.ai-cursor{ display: inline-block; width: 7px; background: #333; height: 16px; margin-bottom: -2px; opacity: 0.95; margin-left: 3px; transition: all 0.3s; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s;}[data-theme=dark].ai-cursor{ background: rgb(255, 255, 255, 0.9);}.ai-btn-box{ font-size: 15.5px; width: 100%; display: flex; flex-direction: row; flex-wrap: wrap;}.ai-btn-item{ padding: 5px 10px; margin: 10px 16px 0px 5px; width: fit-content; line-height: 1; background: rgba(48, 52, 63, 0.75); color: #fff; border-radius: 6px 6px 6px 0; -webkit-border-radius: 6px 6px 6px 0; -moz-border-radius: 6px 6px 6px 0; -ms-border-radius: 6px 6px 6px 0; -o-border-radius: 6px 6px 6px 0; user-select: none; transition: all 0.3s; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s;}.ai-btn-item:hover{ background: #49b0f5dc;}.ai-recommend{ display: flex; flex-direction: row; flex-wrap: wrap;}.ai-recommend-item{ width: 50%; margin-top: 2px;}@media screen and (max-width:768px){ .ai-btn-box{ justify-content: center; } .ai-recommend .ai-recommend-item{ width: 100%; }}.ai-explanation-none{ position: absolute; opacity: 0; width: 0; height: 0; z-index: -999;}使用:在markdown文件的matter添加ai配置,放入ai事先生成好的文章简介
---title: 文章添加AI摘要和推荐ai: 本文介绍了如何通过手动生成GPT网页版摘要,再用JS模拟GPT打字生成效果,实现AI摘要。作者详细讲解了实现思路,包括停顿、延迟等策略,以及如何监听视口并控制生成。作者还提到了未来计划,即使用API生成更多摘要。此外,作者还给出了修改 post.pug 和 CSS 的代码。---1.0版
才两个小时就沦为旧版的屑
修改post.pug,将下面代码加在合适的位置,如 article#article-container.post-content 后,注意缩进
if page.ai .post-ai .ai-title i.fa-brands.fa-airbnb .ai-title-text AI摘要 .ai-tag GPT-4 .ai-explanation #{page.ai} script. if (true) { let animationRunning = true; // 标志变量,控制动画函数的运行 const explanation = document.querySelector('.ai-explanation'); const post_ai = document.querySelector('.post-ai'); const ai_str = explanation.innerHTML; const ai_str_length = ai_str.length; let delay_init = 600; let i = 0; explanation.innerHTML = 'AI摘要生成中. . .'; const animate = (timestamp) => { if (!animationRunning) { return; // 动画函数停止运行 } if (!animate.start) animate.start = timestamp; const elapsed = timestamp - animate.start; if (elapsed >= 20) { animate.start = timestamp; if (i < ai_str_length-1) { let char = ai_str.charAt(i+1); let delay = /[,.,。!?!?]/.test(char) ? 160 : 20; if(explanation.firstElementChild){ explanation.removeChild(explanation.firstElementChild); } explanation.innerHTML += char; let div = document.createElement('div'); div.className = "ai-cursor"; explanation.appendChild(div); i++; if(delay === 160){ document.querySelector('.ai-explanation .ai-cursor').style.opacity = "0"; } if(i === ai_str_length-1){ observer.disconnect();// 暂停监听 explanation.removeChild(explanation.firstElementChild); } setTimeout(() => { requestAnimationFrame(animate); }, delay); } } else { requestAnimationFrame(animate); } }; const observer = new IntersectionObserver((entries) => { let isVisible = entries[0].isIntersecting; animationRunning = isVisible; // 标志变量更新 if(animationRunning){ delay_init = i===0 ? 600 : 20; setTimeout(() => { if(i===0){ explanation.innerHTML = ai_str.charAt(0); } requestAnimationFrame(animate); }, delay_init); } }, { threshold: 0 }); observer.observe(post_ai); }添加CSS,颜色变量F12自取
.post-ai{ background: var(--ai-post-bg); border-radius: 12px; padding: 12px 16px; line-height: 1.3; border: var(--ai-border); margin-top: 10px; margin-bottom: 6px;}.ai-title{ display: flex; color: var(--font-color); border-radius: 8px; align-items: center; padding: 0 5px;}.ai-title i{ font-weight: 800;}.ai-title-text{ font-weight: bold; margin-left: 8px;}.ai-tag{ font-size: 12px; background-color: var(--ai-tag-bg); color: rgba(255,255,255,0.9); border-radius: 4px; margin-left: auto; line-height: 1; padding: 4px 5px;}.ai-explanation{ margin-top: 11px; font-size: 15.5px; line-height: 1.4;}.ai-cursor{ display: inline-block; width: 7px; background: #333; height: 16px; margin-bottom: -2px; opacity: 0.95; margin-left: 3px; transition: all 0.3s; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -ms-transition: all 0.3s; -o-transition: all 0.3s;}[data-theme=dark].ai-cursor{ background: rgb(255, 255, 255, 0.9);}
评论