制作 Chrome 插件
说明:在和 Gemini 对话时,我要求它将一些知识点以 anki 卡片的形式,将正面和反面列举在一个表格中。虽然 Gemini 已经帮我整理好了表格,但是我如果想要导入到 anki 当中,仍然需要不断的复制粘贴,因此想要做一个网页插件
1、提出需求
在我们的对话中,你经常给我 anki 卡片,并且是以表格的形式发给我的,你知道吧。
我想我能不能制作一个 chrome 插件,我的需求是,复制你给我做好的表格,直接导入 anki 当中,这样就不需要我一张张卡片这样复制粘贴了,你觉得可行性怎么样。如果可行的话,能不能列一个清单,让我们把这个插件开发好
2、初步回答
它会一步一步教你如何创建文件,如何在 Chrome 中加载插件…等等
3、进一步明确要求
你能直接给我代码吗,毕竟我只是有这个需求,我不需要懂得是怎么执行的
不断的给它反馈,当前的运行状况即可
4、可运行的满足需求的插件
5、此时可以问他插件还有没有可以优化、改善的地方。它会给你一些建议,你根据他给的那些建议,选几条对你来说最重要的,可以实现的,再让 Gemini 帮忙去实现
6、最终,你得到了一版运行相当不错,并且足以满足需求的插件
7、你甚至可以让 Gemini 帮你写一个 Readme 文档
总结下来就是:1、提想法,提需求;2、一步一步按照它的步骤去做,给它反馈实现的结果;
最终的实现功能:插件会自动查找网页中的 table 表格,连接你的 Anki,并自动注入一个控制条在表格上方,可以选择牌组、选择模板,并一键导入。通用性也很强,像 ChatGPT 这样的内容生成式的,只要内容中包含表格 table 元素,插件也会自动注入
思考
既然 Anki Connect 插件可以连接网页和 Anki,并且上面实现了从网页—>Anki 的过程,那反过来,能不能从 Anki—>网页呢
1、为卡片增加跳转按钮
当你点击时,它会先自动把问题复制到你的剪贴板,然后打开 Gemini。你只需要在 Gemini 窗口按下 Ctrl + V 和 Enter 即可。这比手动选择、复制、切换、粘贴要快得多。
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
|
<hr id="answer">
{{Back}}
<div style="margin-top: 20px;">
<button id="askGemini" style="cursor: pointer; color: white; background: #4285f4; padding: 10px 20px; border: none; border-radius: 8px; font-weight: bold; font-size: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">
🔍 询问 Gemini 深度解析
</button>
</div>
<script>
(function() {
const btn = document.getElementById('askGemini');
const question = "请针对这个知识点给我一个深度解析,并举一个生动的例子:{{text:Front}}";
btn.onclick = function() {
// 1. 核心逻辑:写入剪贴板
const el = document.createElement('textarea');
el.value = question;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
// 2. 视觉反馈:改变按钮文字
const originalText = btn.innerText;
btn.innerText = "✅ 已复制问题,正在跳转...";
btn.style.background = "#34a853";
// 3. 延迟跳转,确保剪贴板写入成功
setTimeout(() => {
window.open("https://gemini.google.com/app", "_blank");
btn.innerText = originalText;
btn.style.background = "#4285f4";
}, 600);
};
})();
</script>
|
- 绕过 URL 限制:不再依赖 Gemini 是否支持
?q= 参数,直接通过你的剪贴板传递。
- 极简操作流:点击按钮 -> 自动跳到 Gemini ->
Ctrl + V。你的大脑不需要处理“复制什么、怎么复制”的过程。
- 多平台通用:如果你想换成 ChatGPT,只需要把代码末尾的
https://gemini.google.com/app 改成 https://chatgpt.com 即可。
缺点也明显,就是问法比较单调,所有问题都只有这一种问法,不满足使用需求
1
|
const question = "请针对这个知识点给我一个深度解析,并举一个生动的例子:{{text:Front}}";
|
2、完善问题指令
既然单一的问法太死板,我们就要把这个按钮升级为一个“智能指令窗口”。我们可以给卡片背面增加一个微型菜单,或者一组快捷指令按钮。这样你就可以根据卡片内容的难易程度,选择不同的 AI 处理方式。
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
|
<hr id="answer">
{{Back}}
<div id="ai-panel" style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 12px; border: 1px solid #e0e0e0; font-family: sans-serif;">
<div style="font-size: 12px; color: #666; margin-bottom: 10px; font-weight: bold; text-align: center;">🤖 选择 AI 学习模式</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<button class="ai-btn" data-prompt="请针对这个知识点给我一个深度解析,并举一个生动的例子:">🧠 深度解析</button>
<button class="ai-btn" data-prompt="请用最通俗易懂、连五岁小孩都能听懂的话解释这个概念:">👶 大白话版</button>
<button class="ai-btn" data-prompt="请针对这个知识点,给我出 3 道考题(填空或选择),带上答案:">📝 自测练习</button>
<button class="ai-btn" data-prompt="请基于这个单词/知识点,造三个不同生活场景的例句:">🎬 场景造句</button>
</div>
</div>
<style>
.ai-btn {
cursor: pointer; background: white; color: #4285f4;
border: 1px solid #4285f4; padding: 8px; border-radius: 6px;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.ai-btn:hover { background: #e8f0fe; transform: translateY(-1px); }
.ai-btn:active { background: #d2e3fc; }
.ai-success { background: #34a853 !important; color: white !important; border-color: #34a853 !important; }
</style>
<script>
(function() {
const buttons = document.querySelectorAll('.ai-btn');
const content = "{{text:Front}}";
buttons.forEach(btn => {
btn.onclick = function() {
const prompt = btn.getAttribute('data-prompt');
const finalQuery = prompt + content;
// 复制到剪贴板
const el = document.createElement('textarea');
el.value = finalQuery;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
// 视觉反馈
const oldText = btn.innerText;
btn.innerText = "✅ 已复制";
btn.classList.add('ai-success');
setTimeout(() => {
window.open("https://gemini.google.com/app", "_blank");
btn.innerText = oldText;
btn.classList.remove('ai-success');
}, 600);
};
});
})();
</script>
|
3、提供上下文
不错,其实我想让点击的时候,也能够体现问题,而不是只有答案,因为如果只是光有卡片背面的内容,你可能都不知道我想要表达什么,也不清楚我当前正在学习什么。所以我觉得你可以将卡片正面和背面都复制好,并且说明,这是 anki 里面的卡片,然后有你所说的几个 data-prompt
这次升级带来的改变:
- 结构化沟通:发给 Gemini 的内容不再是一句光秃秃的话,而是像这样:
- 你好!我正在 Anki 中复习这张卡片… 【卡片正面】:Visual Feedback 【卡片背面】:视觉反馈,指系统… 我的需求是:请针对这张卡片的知识点…
- AI 更聪明了:因为 AI 知道背面内容,它就不会再从零开始猜这个词的意思,而是基于你已经有的解释,去进行升华、举例或出题。
- “考考我”模式:这个特别好用。AI 会看到答案,但它会根据答案反过来问你,从而实现真正的“主动回想”。
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
|
<hr id="answer">
{{Back}}
<div id="ai-panel" style="margin-top: 25px; padding: 15px; background: #ffffff; border-radius: 12px; border: 2px solid #4285f4; font-family: sans-serif; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
<div style="font-size: 13px; color: #4285f4; margin-bottom: 12px; font-weight: bold; text-align: center; border-bottom: 1px solid #eef2f6; padding-bottom: 8px;">
✨ AI 深度助教模式
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<button class="ai-btn" data-mode="深度解析" data-prompt="请针对这张卡片的知识点,结合正反面内容给我一个深度解析,并举一个生动的例子:">🧠 深度解析</button>
<button class="ai-btn" data-mode="大白话" data-prompt="请用最通俗易懂、连五岁小孩都能听懂的话帮我解释这张卡片的概念:">👶 大白话版</button>
<button class="ai-btn" data-mode="模拟面试/考试" data-prompt="请针对这张卡片的内容,像面试官或老师一样问我一个挑战性问题,并等我回答后再给出评价:">📝 考考我</button>
<button class="ai-btn" data-mode="场景扩展" data-prompt="请基于这张卡片的内容,提供 3 个它在现实工作或生活场景中的实际应用案例:">🎬 场景应用</button>
</div>
</div>
<style>
.ai-btn {
cursor: pointer; background: #f8f9fa; color: #333;
border: 1px solid #ddd; padding: 10px 8px; border-radius: 8px;
font-size: 13px; font-weight: 500; transition: all 0.2s;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.ai-btn:hover { background: #e8f0fe; border-color: #4285f4; color: #4285f4; transform: translateY(-2px); }
.ai-success { background: #34a853 !important; color: white !important; border-color: #34a853 !important; }
</style>
<script>
(function() {
const buttons = document.querySelectorAll('.ai-btn');
// 获取当前卡片的上下文信息
const cardFront = "{{text:Front}}";
const cardBack = "{{text:Back}}";
buttons.forEach(btn => {
btn.onclick = function() {
const mode = btn.getAttribute('data-mode');
const promptInstruction = btn.getAttribute('data-prompt');
// 构建结构化的超级提示词
const finalQuery = `你好!我正在 Anki 中复习这张卡片,请作为我的学习助教帮我进行【${mode}】。
---
【卡片正面】:${cardFront}
【卡片背面】:${cardBack}
---
我的需求是:${promptInstruction}`;
// 复制到剪贴板
const el = document.createElement('textarea');
el.value = finalQuery;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
// 视觉反馈
const oldText = btn.innerText;
btn.innerText = "✅ 已合成上下文";
btn.classList.add('ai-success');
setTimeout(() => {
window.open("https://gemini.google.com/app", "_blank");
btn.innerText = oldText;
btn.classList.remove('ai-success');
}, 600);
};
});
})();
</script>
|
4、减少跳转
- 左键点击:执行“复制 + 跳转”(适合还没打开浏览器时)。
- 右键点击:执行“仅复制”(适合浏览器已经打开了 Gemini 窗口,想保持页面不跳动时)。
- 视觉区分:仅复制成功时,按钮会闪烁蓝色;跳转成功时,闪烁绿色。
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
|
<hr id="answer">
{{Back}}
<div id="ai-panel" style="margin-top: 25px; padding: 15px; background: #ffffff; border-radius: 12px; border: 2px solid #4285f4; font-family: sans-serif; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
<div style="font-size: 13px; color: #4285f4; margin-bottom: 5px; font-weight: bold; text-align: center;">
✨ AI 深度助教 V1.2
</div>
<div style="font-size: 11px; color: #999; margin-bottom: 12px; text-align: center;">
左键: 复制+跳转 | 右键: 仅复制
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<button class="ai-btn" data-mode="深度解析" data-prompt="请结合正反面内容给我一个深度解析,并举一个生动的例子:">🧠 深度解析</button>
<button class="ai-btn" data-mode="大白话" data-prompt="请用最通俗易懂的话帮我解释这张卡片的概念:">👶 大白话版</button>
<button class="ai-btn" data-mode="模拟测试" data-prompt="请根据这张卡片问我一个挑战性问题,并等我回答后再评价:">📝 考考我</button>
<button class="ai-btn" data-mode="关联思考" data-prompt="请将这张卡片与我们之前讨论的内容进行关联:">🔗 关联知识</button>
</div>
</div>
<style>
.ai-btn {
cursor: pointer; background: #f8f9fa; color: #333;
border: 1px solid #ddd; padding: 10px 8px; border-radius: 8px;
font-size: 13px; font-weight: 500; transition: all 0.2s;
outline: none;
}
.ai-btn:hover { background: #e8f0fe; border-color: #4285f4; color: #4285f4; }
/* 跳转成功的颜色 */
.ai-success-jump { background: #34a853 !important; color: white !important; border-color: #34a853 !important; }
/* 仅复制成功的颜色 */
.ai-success-copy { background: #4285f4 !important; color: white !important; border-color: #4285f4 !important; }
</style>
<script>
(function() {
const buttons = document.querySelectorAll('.ai-btn');
const cardFront = "{{text:Front}}";
const cardBack = "{{text:Back}}";
buttons.forEach(btn => {
// 构建提示词函数
const getQuery = () => {
const mode = btn.getAttribute('data-mode');
const promptInstruction = btn.getAttribute('data-prompt');
return `你好,请继续作为我的学习助教。这是我正在复习的一张 Anki 卡片。
---
【卡片正面】:${cardFront}
【卡片背面】:${cardBack}
---
任务:【${mode}】 ${promptInstruction}`;
};
const copyText = (text) => {
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
};
// 1. 左键点击:复制 + 跳转
btn.onclick = function() {
copyText(getQuery());
const oldText = btn.innerText;
btn.innerText = "✅ 已复制并跳转";
btn.classList.add('ai-success-jump');
setTimeout(() => {
window.open("https://gemini.google.com/app", "_blank");
btn.innerText = oldText;
btn.classList.remove('ai-success-jump');
}, 400);
};
// 2. 右键点击:仅复制 (阻止菜单弹出)
btn.oncontextmenu = function(e) {
e.preventDefault();
copyText(getQuery());
const oldText = btn.innerText;
btn.innerText = "📋 仅复制成功";
btn.classList.add('ai-success-copy');
setTimeout(() => {
btn.innerText = oldText;
btn.classList.remove('ai-success-copy');
}, 800);
};
});
})();
</script>
|
5、继续优化
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
|
<hr id="answer">
{{Back}}
<div id="ai-launcher">
<div id="ai-icon">🤖</div>
<div id="ai-panel">
<div class="panel-header">✨ AI 深度助教 V1.3</div>
<div class="btn-grid">
<button class="ai-btn" data-mode="深度解析" data-prompt="请结合内容给我深度解析并举例:">🧠 深度解析</button>
<button class="ai-btn" data-mode="自测练习" data-prompt="请针对这个知识点考考我:">📝 考考我</button>
<button class="ai-btn" data-mode="大白话" data-prompt="请用五岁小孩能听懂的话解释:">👶 大白话版</button>
<button class="ai-btn" id="surprise-btn">✨ 给我惊喜</button>
</div>
<div class="footer-hint">左键跳转 | 右键仅复制</div>
</div>
</div>
<style>
/* 悬浮容器 */
#ai-launcher {
position: fixed; bottom: 20px; right: 20px;
z-index: 9999; font-family: sans-serif;
}
/* 默认小图标 */
#ai-icon {
width: 45px; height: 45px; background: #4285f4;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-size: 24px; cursor: pointer; color: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.2); transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* 面板主体 - 初始状态折叠并透明 */
#ai-panel {
position: absolute; bottom: 0; right: 0;
width: 280px; background: white; border-radius: 15px;
border: 2px solid #4285f4; padding: 15px;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
opacity: 0; pointer-events: none; transform: scale(0.8) translateY(20px);
transition: all 0.3s ease; transform-origin: bottom right;
}
/* 悬浮展开逻辑 */
#ai-launcher:hover #ai-panel {
opacity: 1; pointer-events: auto; transform: scale(1) translateY(0);
}
#ai-launcher:hover #ai-icon {
transform: scale(0); opacity: 0;
}
.panel-header { font-size: 14px; font-weight: bold; color: #4285f4; margin-bottom: 12px; text-align: center; }
.btn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.ai-btn {
cursor: pointer; background: #f8f9fa; color: #333; border: 1px solid #ddd;
padding: 8px 5px; border-radius: 8px; font-size: 12px; transition: all 0.2s;
outline: none;
}
.ai-btn:hover { background: #e8f0fe; border-color: #4285f4; color: #4285f4; }
.ai-success-jump { background: #34a853 !important; color: white !important; border-color: #34a853 !important; }
.ai-success-copy { background: #4285f4 !important; color: white !important; border-color: #4285f4 !important; }
.footer-hint { font-size: 10px; color: #999; margin-top: 10px; text-align: center; }
</style>
<script>
(function() {
const buttons = document.querySelectorAll('.ai-btn');
const surpriseBtn = document.getElementById('surprise-btn');
const cardFront = "{{text:Front}}";
const cardBack = "{{text:Back}}";
// 方案 B:随机指令池
const surprises = [
{ mode: "幽默大师", prompt: "请用极其幽默甚至有点损的风格帮我解读这个知识点:" },
{ mode: "武侠风", prompt: "请把这个知识点想象成一门武林绝学,用武侠小说的风格解释它:" },
{ mode: "科幻风", prompt: "请把这个知识点放在 2077 年的赛博朋克背景下,描述它的应用:" },
{ mode: "苏格拉底", prompt: "请不要直接解释,而是通过连续问我 3 个引导性问题,让我自己悟出这个知识点:" },
{ mode: "辩论赛", prompt: "请针对这个知识点,找出一个反直觉的观点,并尝试跟我辩论:" }
];
const copyText = (text) => {
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
};
const handleAction = (btn, isJump) => {
let mode, promptStr;
if (btn.id === 'surprise-btn') {
const randomItem = surprises[Math.floor(Math.random() * surprises.length)];
mode = randomItem.mode;
promptStr = randomItem.prompt;
} else {
mode = btn.getAttribute('data-mode');
promptStr = btn.getAttribute('data-prompt');
}
const finalQuery = `你好!我正在复习一张 Anki 卡片。
---
【卡片正面】:${cardFront}
【卡片背面】:${cardBack}
---
任务:【${mode}】 ${promptStr}`;
copyText(finalQuery);
const oldText = btn.innerText;
if (isJump) {
btn.innerText = "✅ 复制并跳转";
btn.classList.add('ai-success-jump');
setTimeout(() => {
window.open("https://gemini.google.com/app", "AnkiAIAssistant");
btn.innerText = oldText;
btn.classList.remove('ai-success-jump');
}, 400);
} else {
btn.innerText = "📋 仅复制成功";
btn.classList.add('ai-success-copy');
setTimeout(() => {
btn.innerText = oldText;
btn.classList.remove('ai-success-copy');
}, 800);
}
};
buttons.forEach(btn => {
btn.onclick = () => handleAction(btn, true);
btn.oncontextmenu = (e) => { e.preventDefault(); handleAction(btn, false); };
});
})();
</script>
|
- 极简界面:复习时,你只会看到右下角有一个静静悬浮的蓝色机器人图标 🤖,完全不干扰阅读卡片。
- 优雅交互:当你需要 AI 时,鼠标滑过去,面板就会像抽屉一样“弹”出来。
- 随机惊喜:如果觉得复习太累了,点一下 ✨ 给我惊喜。可能 AI 会突然用扫地僧的口吻教你这个知识点,这种意外的幽默感是缓解学习焦虑的良药。
我一开始还以为只有做一个 anki 插件才能实现这样的功能呢,没想到直接在模板中就可以增加
为什么“模板方案”比“插件方案”更优雅:
实际上,Anki 的渲染引擎是一个完整的 Chromium 核。这意味着,你可以在卡片背面运行几乎任何网页技术:HTML5、CSS3 以及 JavaScript。
| 维度 |
Anki 插件 (Add-on) |
卡片模板 (Template) —— 我们的方案 |
| 开发难度 |
需要学习 Python 和 Qt 框架 |
只需前端基础 (HTML/JS/CSS) |
| 跨平台性 |
仅限电脑桌面端,手机端无效 |
全平台通用(电脑、安卓、iOS 都能跑 JS) |
| 同步性 |
换一台电脑需要重新安装插件 |
随牌组自动同步,只要登录账号,代码全都在 |
| 维护成本 |
Anki 版本更新经常导致插件失效 |
极低,只要浏览器还支持 JS,它就永远有效 |