前言

@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和浏览器缓存的影响,建议指定资源版本号使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 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的引入的位置应该在文章内容之后

1
2
3
4
5
6
7
8
<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实时简介

1
2
3
4
5
6
//默认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模块的文章就删去这个配置项

1
2
3
4
---
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 后,注意缩进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
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更新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
.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 后,注意缩进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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自取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
.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事先生成好的文章简介

1
2
3
4
5
---
title: 文章添加AI摘要和推荐
ai: 本文介绍了如何通过手动生成GPT网页版摘要,再用JS模拟GPT打字生成效果,实现AI摘要。作者详细讲解了实现思路,包括停顿、延迟等策略,以及如何监听视口并控制生成。作者还提到了未来计划,即使用API生成更多摘要。此外,作者还给出了修改 post.pug 和 CSS 的代码。
---

1.0版

才两个小时就沦为旧版的屑
修改post.pug,将下面代码加在合适的位置,如 article#article-container.post-content 后,注意缩进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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自取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
.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);
}