轻笑Chuckle
外观

项目

文章摘要、相关推荐生成AI

给文章加上AI生成的摘要和相关文章的推荐,打字机效果模拟ChatGPT

6 分钟 2,408 字 次浏览 条评论 更新于 2023年8月25日

前言

@Heo又整了新活,给文章加上了AI,我也开写,竞速。

项目地址Post-Summary-AI

大半夜领先一点就睡不着的屑Heo

有这些特性:

  1. 真AI!,接入tianliGPT,JS动态获取全文所有纯文本,传给api实时获取文章摘要(受限于tianliGPT的成本,目前仍有缓存机制)
  2. 基于tianliGPT标注关键词、Python分析相关度的相关文章AI推荐
  3. 遇到标点符号慢下来,动态打字速度
  4. 使用requestAnimationFrame优化性能,动态清除setTimeout、打断fetch请求,交互放心
  5. 使用IntersectionObserver监听,当容器在视口不可见后停止生成摘要,重新出现在视口后继续生成
  6. 模拟GPT的光标效果
  7. 多个按钮实现多个功能,自我介绍、文章摘要、推荐相关文章等
  8. 良好兼容性,性能消耗小。
  9. 适配pjax
  10. 简单引入js后,即可生成QX-AI并自动挂载初始化
  11. 配置项多样,高度自定义
  12. 支持切换摘要、摘要语音朗读

未来的计划:

  1. 目前数据库文章量已经够多,计划构建一个博文社区

5.0积累更新

待 5. 版本稳定后发布 6.0 版本*

项目地址Post-Summary-AI

更多详情请看项目仓库README

新特性:

  1. 优化文章内容的获取
  2. 新增适配pjax的配置项
  3. 自定义css配置项
  4. 添加svg图标
  5. 新增文章截取相关配置项
  6. 更新CSS,优化UI样式
  7. 新增注入额外CSS配置项
  8. 新增切换简介功能及相关配置
  9. 新增摘要语音朗读功能
  10. 新增UI文字自定义配置
  11. 新增页面排除配置项
  12. 标识访客唯一ID
  13. 新增直接显示摘要的配置项
  14. 优化JS的业务逻辑,精简代码
  15. 新增自动挂载:自动获取文章内容所在容器元素,而无需el配置项
  16. 新增白名单:只让指定页面显示摘要AI
  17. js动态插入css,无需额外引入
  18. 新增控制打字机效果的配置项,包括:是否启用打字机效果、打字速度
  19. 新增矩阵穿梭功能及按钮,矩阵穿梭:随机前往一个部署了AI摘要的网站
  20. 新增是否启用矩阵穿梭功能的配置项,默认开启

4.0重磅更新

新特性:

  1. 新增相关文章AI推荐
  2. 引入缓存机制,减轻后端压力
  3. 优化代码及性能

快速上手

项目地址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({ /* 传入配置对象 */ }) 详解

  1. el 文章内容所在的元素属性的选择器,也是AI挂载的容器,AI将会挂载到该容器的最前面
  2. key 驱动AI所必须的key,即是tianliGPT后端服务所必须的key
  3. title_el 文章标题所在的元素属性的选择器,默认获取当前网页的标题
  4. rec_method 文章推荐方式,all:匹配数据库内所有文章进行推荐,web:仅当前站内的文章,默认all
  5. exclude 获取文章内容时,需要排除的元素及其子元素

tianliGPT的key请到爱发电中购买,10元5万字符(常有优惠)。请求过的内容再次请求不会消耗key,可以无限期使用。

更多详情请看项目仓库README

3.0全新版本

新特性:

  1. 简单引入js后,即可生成QX-AI并自动挂载初始化
  2. 新增项目地址Post-Summary-AI
  3. 优化代码逻辑,引入外部配置

快速上手

非常简单,引入下面这些代码到你的网站内即可,其中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配置详解:

  1. el 文章内容所在的元素属性的选择器,也是AI挂载的容器,AI将会挂载到该容器的最前面
  2. 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,记得配置Key
const 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版

新特性:

  1. 真!AI,接入tianliGPT或是使用官方api接口,随意选择
  2. 前端限制请求频率、动态打断fetch
  3. 压缩文本降低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);
}

评论

快速前往