4 |
5 | {[...Array(totalSteps)].map((_, index) => (
6 |
onChange?.(index + 1)}
9 | className={`h-1 flex-1 rounded-full transition-colors ${
10 | index < currentStep ? "bg-indigo-500" : "bg-indigo-200/70"
11 | } ${index + 1 > currentStep ? "cursor-not-allowed" : "cursor-pointer"}`}
12 | />
13 | ))}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/components/Welcome/modules/confirmModal.js:
--------------------------------------------------------------------------------
1 | const ConfirmModal = ({ isOpen, onClose, onConfirm }) => {
2 | if (!isOpen) return null;
3 |
4 | return (
5 |
6 |
7 |
8 |
12 |
13 |
14 | 正在导入您的书签数据,这可能需要一些时间,您可以将此界面放在后台,浏览其他页面。
15 |
16 |
17 | 请保持浏览器保持在打开状态,等待导入完成后点击知道了。
18 |
19 |
20 |
21 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export { ConfirmModal };
35 |
--------------------------------------------------------------------------------
/components/Welcome/modules/logo.js:
--------------------------------------------------------------------------------
1 | export function Logo({ className }) {
2 | return (
3 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/components/Welcome/service/login.js:
--------------------------------------------------------------------------------
1 | import { config } from "../../config/index";
2 |
3 | export const getCode = async (email) => {
4 | try {
5 | const response = await fetch(`${config.baseUrl}/auth/send-code`, {
6 | method: "POST",
7 | headers: {
8 | "Content-Type": "application/json",
9 | },
10 | body: JSON.stringify({ email }),
11 | });
12 |
13 | if (!response.ok) {
14 | throw new Error(`获取邮件失败,请重试`);
15 | }
16 |
17 | const data = await response.json();
18 |
19 | // 验证返回数据的格式
20 | if (data.code !== 200) {
21 | throw new Error("获取验证码失败,请检查网络链接");
22 | }
23 |
24 | return data.data;
25 | } catch (error) {
26 | console.error("获取验证码出错:", error);
27 | throw error;
28 | }
29 | };
30 |
31 | export const login = async ({ email, code }) => {
32 | try {
33 | const response = await fetch(`${config.baseUrl}/auth/login`, {
34 | method: "POST",
35 | headers: {
36 | "Content-Type": "application/json",
37 | },
38 | body: JSON.stringify({ email, code }),
39 | });
40 |
41 | if (!response.ok) {
42 | throw new Error(`验证码错误或者已过期`);
43 | }
44 |
45 | const data = await response.json();
46 |
47 | if (data.code !== 200) {
48 | throw new Error("登录失败,请检查网络链接");
49 | }
50 |
51 | return data.data;
52 | } catch (error) {
53 | console.error("登录失败:", error);
54 | throw error;
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/components/Welcome/step/FirstRight.js:
--------------------------------------------------------------------------------
1 | const FirstRight = () => {
2 | const features = [
3 | {
4 | icon: "📥",
5 | title: "智能网页采集",
6 | description: "一键保存网页内容,自动构建知识向量库",
7 | },
8 | {
9 | icon: "🤖",
10 | title: "AI智能问答",
11 | description: "接入官方及第三方大模型,实现知识库智能对话",
12 | },
13 | {
14 | icon: "🔎",
15 | title: "联网实时搜索",
16 | description: "智能联网搜索,为对话提供最新信息支持",
17 | },
18 | {
19 | icon: "📃",
20 | title: "页面速览",
21 | description: "快速提取页面重点内容,提升阅读效率",
22 | },
23 | {
24 | icon: "🗨️",
25 | title: "智能对话",
26 | description: "与当前页面内容进行智能对话交互,深入理解页面信息",
27 | },
28 | ];
29 |
30 | return (
31 |
32 |
33 |
34 | 🚀 打造智能网页助手
35 |
36 |
构建个人知识库,提升网页浏览体验
37 |
38 |
39 |
40 | {features.map(({ icon, title, description }) => (
41 |
45 |
{icon}
46 |
47 |
{title}
48 |
{description}
49 |
50 |
51 | ))}
52 |
53 |
54 | );
55 | };
56 |
57 | export default FirstRight;
58 |
--------------------------------------------------------------------------------
/components/Welcome/step/Second.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const RefreshModal = ({ isOpen, onClose, onRefresh }) => {
4 | if (!isOpen) return null;
5 |
6 | return (
7 |
8 |
9 |
13 |
需要刷新标签页
14 |
15 | 由于浏览器限制,必须刷新当前已打开的标签页才能使用 Super2Brain。 新打开的标签页无需刷新。
16 |
17 |
点击下面的按钮后会关闭当前网页
18 |
19 |
26 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default function Second({ onNext }) {
40 | const [isRefreshModalOpen, setIsRefreshModalOpen] = useState(true);
41 |
42 | const handleRefresh = () => {
43 | chrome.runtime.sendMessage(
44 | {
45 | action: "refreshAllTabs",
46 | bypassCache: true,
47 | },
48 | (response) => {
49 | if (response.success) {
50 | setIsRefreshModalOpen(false);
51 | window.close();
52 | } else {
53 | console.error("刷新标签页失败:", response.error);
54 | window.close();
55 | }
56 | }
57 | );
58 | };
59 |
60 | return (
61 |
62 | {
65 | setIsRefreshModalOpen(false);
66 | window.close();
67 | }}
68 | onRefresh={handleRefresh}
69 | />
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/components/Welcome/step/SecondRight.js:
--------------------------------------------------------------------------------
1 | const SecondRight = () => {
2 | const features = [
3 | {
4 | icon: "🔖",
5 | title: "书签智能导入",
6 | description: "一键导入浏览器书签,批量构建个人知识库",
7 | },
8 | {
9 | icon: "🎯",
10 | title: "RAG知识库构建",
11 | description: "自动向量化网页内容,构建高质量问答数据库",
12 | },
13 | {
14 | icon: "🤖",
15 | title: "多模型智能问答",
16 | description: "支持ChatGPT、Claude等模型,实现知识库智能对话",
17 | },
18 | {
19 | icon: "💬",
20 | title: "实时网页对话",
21 | description: "与当前浏览的网页内容实时对话,快速获取页面要点",
22 | },
23 | {
24 | icon: "🔍",
25 | title: "智能网页解析",
26 | description: "一键分析任意网页内容,基于RAG技术进行深度问答交互",
27 | },
28 | ];
29 |
30 | return (
31 |
32 |
33 |
34 | 🚀 打造智能网页助手
35 |
36 |
构建个人知识库,提升网页浏览体验
37 |
38 |
39 |
40 | {features.map(({ icon, title, description }) => (
41 |
45 |
{icon}
46 |
47 |
{title}
48 |
{description}
49 |
50 |
51 | ))}
52 |
53 |
54 | );
55 | };
56 |
57 | export default SecondRight;
58 |
--------------------------------------------------------------------------------
/components/config/index.js:
--------------------------------------------------------------------------------
1 | export const config = {
2 | baseUrl: "https://s2bapi.zima.pet",
3 | };
4 |
--------------------------------------------------------------------------------
/hooks/useChat.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { getResponse } from "../utils/index.js";
3 |
4 | export const useChat = (useInput = false) => {
5 | const [state, setState] = useState({
6 | isOpen: false,
7 | model: "gpt-4",
8 | query: "",
9 | loading: false,
10 | messages: [],
11 | expandedDocs: {},
12 | copiedMessageId: null,
13 | });
14 |
15 | // 使用函数式更新state
16 | const updateState = useCallback((updates) => {
17 | setState((prev) => ({ ...prev, ...updates }));
18 | }, []);
19 |
20 | // 更新消息列表的工具函数
21 | const updateMessage = useCallback((messageId, updates) => {
22 | setState((prev) => ({
23 | ...prev,
24 | messages: prev.messages.map((msg) => (msg.id === messageId ? { ...msg, ...updates } : msg)),
25 | }));
26 | }, []);
27 |
28 | // 处理提交
29 | const handleSubmit = useCallback(async () => {
30 | if (!useInput || !state.query.trim() || state.loading) return;
31 |
32 | const userMessage = {
33 | id: Date.now().toString(),
34 | content: state.query,
35 | isUser: true,
36 | isComplete: true,
37 | isClosed: false,
38 | };
39 |
40 | const aiMessage = {
41 | id: (Date.now() + 1).toString(),
42 | content: "",
43 | isUser: false,
44 | isComplete: false,
45 | isClosed: false,
46 | related: [],
47 | aboutQuestion: [],
48 | questionsLoading: true,
49 | };
50 |
51 | updateState({
52 | messages: [...state.messages, userMessage, aiMessage],
53 | query: "",
54 | loading: true,
55 | });
56 |
57 | try {
58 | await getResponse(
59 | state.query,
60 | state.messages.map((msg) => ({
61 | role: msg.isUser ? "user" : "assistant",
62 | content: msg.content,
63 | })),
64 | (progress) => {
65 | if (progress.stage === 2 && progress.results) {
66 | updateMessage(aiMessage.id, { related: progress.results });
67 | } else if (progress.stage === 3 && progress.response) {
68 | updateMessage(aiMessage.id, {
69 | content: progress.response,
70 | isClosed: true,
71 | isComplete: true,
72 | questionsLoading: true,
73 | });
74 | } else if (progress.stage === 5 && progress.questions) {
75 | updateMessage(aiMessage.id, {
76 | aboutQuestion: progress.questions,
77 | questionsLoading: false,
78 | });
79 | }
80 | }
81 | );
82 | } catch (error) {
83 | console.error("对话请求失败:", error);
84 | } finally {
85 | updateState({ loading: false });
86 | }
87 | }, [useInput, state.query, state.loading, state.messages, updateState, updateMessage]);
88 |
89 | // 处理复制
90 | const handleCopy = useCallback(async (content, messageId) => {
91 | try {
92 | await navigator.clipboard.writeText(content);
93 | updateState({ copiedMessageId: messageId });
94 | setTimeout(() => updateState({ copiedMessageId: null }), 1500);
95 | } catch (err) {
96 | console.error("复制失败:", err);
97 | }
98 | }, []);
99 |
100 | // 处理重新生成
101 | const handleRegenerate = useCallback(
102 | async (messageId) => {
103 | const currentIndex = state.messages.findIndex((msg) => msg.id === messageId);
104 | if (currentIndex < 1) return;
105 |
106 | const userQuestion = state.messages[currentIndex - 1].content;
107 | updateState({ loading: true });
108 | updateMessage(messageId, {
109 | content: "",
110 | questionsLoading: false,
111 | isComplete: false,
112 | });
113 |
114 | try {
115 | await getResponse(
116 | userQuestion,
117 | state.messages.slice(0, currentIndex - 1).map((msg) => ({
118 | role: msg.isUser ? "user" : "assistant",
119 | content: msg.content,
120 | })),
121 | (progress) => {
122 | if (progress.stage === 2 && progress.results) {
123 | updateMessage(messageId, { related: progress.results });
124 | } else if (progress.stage === 3 && progress.response) {
125 | updateMessage(messageId, {
126 | content: progress.response,
127 | isClosed: true,
128 | });
129 | } else if (progress.stage === 5 && progress.questions) {
130 | updateMessage(messageId, { aboutQuestion: progress.questions });
131 | }
132 | }
133 | );
134 | } catch (error) {
135 | console.error("重新生成失败:", error);
136 | } finally {
137 | updateState({ loading: false });
138 | }
139 | },
140 | [state.messages, updateState, updateMessage]
141 | );
142 |
143 | // 重置对话
144 | const handleReset = useCallback(() => {
145 | updateState({
146 | messages: [],
147 | query: "",
148 | });
149 | }, [updateState]);
150 |
151 | return {
152 | ...state,
153 | setModel: (model) => updateState({ model }),
154 | setQuery: (query) => updateState({ query }),
155 | setIsOpen: (isOpen) => updateState({ isOpen }),
156 | setExpandedDocs: (expandedDocs) => updateState({ expandedDocs }),
157 | handleSubmit,
158 | handleCopy,
159 | handleRegenerate,
160 | handleReset,
161 | };
162 | };
163 |
--------------------------------------------------------------------------------
/hooks/useMessageHandler.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "deepseek-r1",
4 | "token": 0.6000,
5 | "price": 0.3000
6 | },
7 | {
8 | "name": "deepseek-v3",
9 | "token": 0.5620,
10 | "price": 0.281
11 | },
12 | {
13 | "name": "ernie-speed-128",
14 | "token": 0.1500,
15 | "price": 0.0750
16 | },
17 | {
18 | "name": "claude-3-5-sonnet-20240620",
19 | "token": 15.0000,
20 | "price": 5.0000
21 | },
22 | {
23 | "name": "gpt-4o-mini",
24 | "token": 0.1500,
25 | "price": 0.075
26 | },
27 | {
28 | "name": "gpt-4o",
29 | "token": 5.0,
30 | "price": 2.5000
31 | },
32 | {
33 | "name": "glm-4v-32k",
34 | "token": 6.6700,
35 | "price": 3.335
36 | },
37 | {
38 | "name": "glm-4v",
39 | "token": 6.6700,
40 | "price": 3.3335
41 | },
42 | {
43 | "name": "qwen-max",
44 | "token": 0.2700,
45 | "price": 0.135
46 | },
47 | {
48 | "name": "qwen-turbo",
49 | "token": 60.0000,
50 | "price": 0.135
51 | },
52 | {
53 | "name": "Doubao-pro-128k",
54 | "token": 7.0000,
55 | "price": 3.5
56 | },
57 | {
58 | "name": "Doubao-lite-128k",
59 | "token": 1.1200,
60 | "price": 0.56
61 | },
62 | {
63 | "name": "claude-3-opus-20240229",
64 | "token": 15.0000,
65 | "price": 7.5
66 | }
67 | ]
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/out.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const glob = require("glob");
3 |
4 | const files = glob.sync("out/**/*.html");
5 | files.forEach((file) => {
6 | const content = fs.readFileSync(file, "utf-8");
7 | const modifiedContent = content.replace(/\/_next/g, "./next");
8 | fs.writeFileSync(file, modifiedContent, "utf-8");
9 | });
10 |
11 | const sourcePath = "out/_next";
12 | const destinationPath = "out/next";
13 |
14 | fs.rename(sourcePath, destinationPath, (err) => {
15 | if (err) {
16 | console.error('Failed to rename "_next" directory to "next".', err);
17 | } else {
18 | console.log('Renamed "_next" directory to "next" successfully.');
19 | }
20 | });
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Super2Brain",
3 | "version": "1.0.0",
4 | "description": "Super2Brain",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "npm run prep",
8 | "prep": "npm run exp && node out.js",
9 | "exp": "next build && next export",
10 | "run": "next start",
11 | "dev": "next dev"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "@ai-sdk/openai": "^1.1.13",
18 | "@fortawesome/fontawesome-svg-core": "^6.5.2",
19 | "@fortawesome/free-solid-svg-icons": "^6.5.2",
20 | "@fortawesome/react-fontawesome": "^0.2.2",
21 | "@mozilla/readability": "^0.5.0",
22 | "better-react-mathjax": "^2.1.0",
23 | "d3": "^7.9.0",
24 | "docx": "^9.2.0",
25 | "file-saver": "^2.0.5",
26 | "framer-motion": "^12.0.11",
27 | "glob": "^10.2.3",
28 | "html2canvas": "^1.4.1",
29 | "html2pdf.js": "^0.10.2",
30 | "js-tiktoken": "^1.0.19",
31 | "jspdf": "^2.5.2",
32 | "katex": "^0.16.21",
33 | "lucide-react": "^0.377.0",
34 | "markdown-it": "^14.1.0",
35 | "markdown-it-deflist": "^3.0.0",
36 | "markdown-it-footnote": "^4.0.0",
37 | "markdown-it-mark": "^4.0.0",
38 | "markdown-it-sub": "^2.0.0",
39 | "markdown-it-sup": "^2.0.0",
40 | "marked": "^15.0.5",
41 | "markmap-view": "^0.18.5",
42 | "mathjax-react": "^2.0.1",
43 | "next": "^13.4.1",
44 | "openai": "^4.82.0",
45 | "react": "^18.2.0",
46 | "react-checkbox-tree": "^1.8.0",
47 | "react-dom": "^18.2.0",
48 | "react-icons": "^5.4.0",
49 | "react-katex": "^3.0.1",
50 | "react-tooltip": "^5.28.0",
51 | "rehype-katex": "^7.0.1",
52 | "remark-math": "^6.0.0",
53 | "tailwind-scrollbar": "4.0.0-beta.0",
54 | "turndown": "^7.2.0"
55 | },
56 | "devDependencies": {
57 | "@tailwindcss/typography": "^0.5.16",
58 | "@types/react": "19.0.10",
59 | "autoprefixer": "^10.4.19",
60 | "postcss": "^8.4.39",
61 | "tailwindcss": "^3.4.4",
62 | "typescript": "5.8.2"
63 | }
64 | }
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 |
3 | export default function App({ Component, pageProps }) {
4 | return (
5 | <>
6 |
7 | >
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/pages/sidepanel.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Sidebar from "../components/Sidebar";
3 |
4 | export default function SidePanel() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/pages/welcome.js:
--------------------------------------------------------------------------------
1 | import WelcomeCom from "../components/Welcome/index";
2 |
3 | export default function Welcome() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/point.json:
--------------------------------------------------------------------------------
1 | {
2 | "glm-4": 40,
3 | "glm-4v": 40,
4 | "qwen-max": 40,
5 | "qwen-turbo": 40,
6 | "doubao-pro": 40,
7 | "doubao-lite": 40,
8 | "gpt-4.5-preview": 600,
9 | "gpt-4.5-preview-2025-02-27": 600,
10 | "gpt-4o": 120,
11 | "gpt-4o-mini": 30,
12 | "ernie-speed-128": 40,
13 | "deepseek-r1": 80,
14 | "deepseek-v3": 60,
15 | "grok-3": 60,
16 | "claude-3-7-sonnet-20250219": 500,
17 | "claude-3-7-sonnet-thinking": 500,
18 | "claude-3.5-sonnet": 100,
19 | "claude-3-opus": 100
20 | }
--------------------------------------------------------------------------------
/pointPost.json:
--------------------------------------------------------------------------------
1 | {
2 | "GLM-4": 40,
3 | "GLM-4V": 40,
4 | "Qwen-Max": 40,
5 | "Qwen-Turbo": 40,
6 | "Doubao-Pro": 40,
7 | "Doubao-Lite": 40,
8 | "Ernie-Speed-128": 40,
9 | "GPT-4.5-Preview": 600,
10 | "GPT-4.5-Preview-2025-02-27": 600,
11 | "GPT-4o": 120,
12 | "GPT-4o-Mini": 30,
13 | "Deepseek-R1": 80,
14 | "Deepseek-V3": 60,
15 | "Grok-3": 60,
16 | "Claude-3-7-Sonnet-Thinking": 500,
17 | "Claude-3-7-Sonnet-20250219": 500,
18 | "Claude-3-5-Sonnet": 100,
19 | "Claude-3-Opus": 100
20 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/content.js:
--------------------------------------------------------------------------------
1 | async function extractArticleContent() {
2 | let article = document.querySelector("article");
3 | if (!article) {
4 | article = document.body;
5 | }
6 | return article.innerText;
7 | }
8 |
9 | function callLLM(content) {
10 | const apiUrl = `https://s2bapi.zima.pet/text/v1/mindmap/chat/completions`;
11 |
12 | fetch(apiUrl, {
13 | method: "POST",
14 | headers: {
15 | "Content-Type": "application/json",
16 | Authorization: `Bearer sk-OSqhqCm1DoE24Kf0E2796eAeE75b484d9f08CbD779E7870a`,
17 | },
18 | body: JSON.stringify({
19 | model: "gpt-4o-mini",
20 | messages: [
21 | { role: "system", content: "You are a helpful assistant." },
22 | {
23 | role: "user",
24 | content: `作为一名专业的文档编辑专家,您需要具备以下技能和完成以下任务:
25 | 1. 分析用户给的文章,提取出文章中的主要内容和结构。
26 | 2. 将文章中的主要内容和结构转换为markdown格式。
27 | 3. 以markdown格式输出。
28 | 4. 输出格式参考如下:请严格按照以下格式输出
29 |
30 | 参考格式:
31 | # 一级标题
32 | ## 二级标题
33 | - 节点内容1
34 | - 节点内容2
35 | - 节点内容3
36 | ## 二级标题
37 | - 节点内容1
38 | - 节点内容2
39 | - 子节点内容1
40 | - 子节点内容2
41 | ...
42 |
43 | 以下是用户给定的参考文章:
44 | :\n\n${content}\n\n以markdown格式输出
45 | `,
46 | },
47 | ],
48 | stream: true,
49 | }),
50 | })
51 | .then((response) => {
52 | const reader = response.body.getReader();
53 | const decoder = new TextDecoder("utf-8");
54 | let text = "";
55 |
56 | function readStream() {
57 | reader.read().then(({ done, value }) => {
58 | if (done) {
59 | chrome.runtime.sendMessage({
60 | action: "updateMindmap",
61 | content: text,
62 | });
63 | return;
64 | }
65 |
66 | const chunk = decoder.decode(value, { stream: true });
67 | const lines = chunk.split("\n").filter((line) => line.trim() !== "");
68 |
69 | for (const line of lines) {
70 | if (line.startsWith("data: ")) {
71 | const jsonString = line.slice(6);
72 | if (jsonString !== "[DONE]") {
73 | try {
74 | const json = JSON.parse(jsonString);
75 | const deltaContent = json.choices[0].delta.content;
76 | text += deltaContent;
77 | chrome.runtime.sendMessage({
78 | action: "updateMindmap",
79 | content: text,
80 | });
81 | } catch (e) {
82 | console.error("Error parsing JSON:", e);
83 | }
84 | }
85 | }
86 | }
87 |
88 | readStream();
89 | });
90 | }
91 |
92 | readStream();
93 | })
94 | .catch((err) => {
95 | console.error("Error calling DeepSeek API:", err);
96 | });
97 | }
98 |
99 | const content = extractArticleContent();
100 | chrome.runtime.sendMessage({ action: "openMindmap" });
101 | callLLM(content);
102 |
--------------------------------------------------------------------------------
/public/icons/logo-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shareAI-lab/Super2Brain-Extension/147f30193af983643e3a6bf77e56a210c0a91be3/public/icons/logo-128.png
--------------------------------------------------------------------------------
/public/icons/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shareAI-lab/Super2Brain-Extension/147f30193af983643e3a6bf77e56a210c0a91be3/public/icons/logo.png
--------------------------------------------------------------------------------
/public/index.css:
--------------------------------------------------------------------------------
1 | .flot-btn {
2 | position: fixed;
3 | right: 0px;
4 | bottom: 200px;
5 | z-index: 1000;
6 | width: 80px;
7 | height: 45px;
8 | border-top-left-radius: 30px;
9 | border-bottom-left-radius: 30px;
10 | color: #fff;
11 | font-size: 14px;
12 | opacity: 1;
13 | cursor: pointer;
14 | transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
15 | transform: translateX(35px);
16 | background: white !important;
17 | margin: 1px;
18 | display: flex;
19 | align-items: center;
20 | justify-content: flex-end;
21 | padding-right: 35px;
22 | outline: none;
23 | border: none;
24 | box-shadow: none;
25 | }
26 |
27 | .flot-btn:hover {
28 | opacity: 1;
29 | transform: translateX(20px);
30 | background: white !important;
31 | }
32 |
33 | .flot-btn svg {
34 | width: 60px;
35 | height: 60px;
36 | }
37 |
38 | .memfree-loader {
39 | animation: spin 1s linear infinite;
40 | }
41 |
42 | @keyframes spin {
43 | 0% {
44 | transform: rotate(0deg);
45 | }
46 | 100% {
47 | transform: rotate(360deg);
48 | }
49 | }
50 |
51 | #custom-alert-container {
52 | position: fixed;
53 | top: 0;
54 | left: 0;
55 | width: 100%;
56 | height: 100%;
57 | display: none;
58 | align-items: center;
59 | justify-content: center;
60 | background: rgba(0, 0, 0, 0.5);
61 | z-index: 1001;
62 | }
63 |
64 | #custom-alert {
65 | background: white;
66 | border-radius: 10px;
67 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
68 | max-width: 400px;
69 | width: 80%;
70 | padding: 50px 20px;
71 | text-align: center;
72 | }
73 |
74 | #custom-alert-content p {
75 | margin: 0 0 20px;
76 | }
77 |
78 | #custom-alert-ok {
79 | background: #3b82f6;
80 | color: white;
81 | border: none;
82 | padding: 10px 20px;
83 | border-radius: 5px;
84 | cursor: pointer;
85 | transition: background 0.3s;
86 | width: 100%;
87 | max-width: 100px;
88 | }
89 |
90 | #custom-alert-ok:hover {
91 | background: #2563eb;
92 | }
93 |
94 | #custom-alert-content a {
95 | color: #3b82f6;
96 | text-decoration: none;
97 | font-weight: 500;
98 | }
99 |
100 | #custom-alert-content a:hover {
101 | text-decoration: underline;
102 | }
103 |
104 | .floating-icon {
105 | position: absolute;
106 | width: 32px;
107 | height: 32px;
108 | background: white;
109 | border-radius: 50%;
110 | top: 50%;
111 | left: 30%;
112 | transform: translate(-50%, -50%) scale(0);
113 | opacity: 0;
114 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
115 | display: flex;
116 | align-items: center;
117 | justify-content: center;
118 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
119 | }
120 |
121 | .floating-icon svg {
122 | width: 20px;
123 | height: 20px;
124 | color: #600db6;
125 | }
126 |
127 | .float-up.flot-btn:hover .floating-icon {
128 | opacity: 1;
129 | top: -40px;
130 | transform: translate(-50%, 0) scale(1);
131 | }
132 |
133 | .float-down.flot-btn:hover .floating-icon {
134 | opacity: 1;
135 | top: 140%;
136 | transform: translate(-50%, 0) scale(1);
137 | }
138 |
--------------------------------------------------------------------------------
/public/inject.js:
--------------------------------------------------------------------------------
1 | const loadingButtonContent = `
2 |
`;
3 |
4 | (function () {
5 | // 确保 DOM 已完全加载
6 | const initializeExtension = () => {
7 | if (window._sidebarInjected) return;
8 | window._sidebarInjected = true;
9 |
10 | if (typeof Readability === "undefined" || typeof TurndownService === "undefined") {
11 | console.error("必要的依赖未加载");
12 | return;
13 | }
14 | if (!document.body) {
15 | console.error("DOM 未完全加载");
16 | return;
17 | }
18 |
19 | window.addEventListener("message", function (event) {
20 | if (event.data.type === "SIDEBAR_ACTION") {
21 | chrome.runtime.sendMessage({ action: "openSidebar" });
22 | }
23 | });
24 |
25 | window.openSuperBrainSidebar = function () {
26 | window.postMessage({ type: "SIDEBAR_ACTION" }, "*");
27 | };
28 |
29 | if (document.getElementById("send-url-button")) return;
30 |
31 | const button = document.createElement("button");
32 | button.id = "send-url-button";
33 | button.className = "flot-btn";
34 |
35 | button.disabled = false;
36 |
37 | const svgButtonContent = `
38 |
39 |
48 | `;
49 |
50 | button.innerHTML = svgButtonContent;
51 |
52 | button.addEventListener("mousedown", function (e) {
53 | e.preventDefault();
54 |
55 | let startY = e.clientY - button.offsetTop;
56 | document.addEventListener("mousemove", onMouseMove);
57 | document.addEventListener("mouseup", onMouseUp);
58 |
59 | function onMouseMove(e) {
60 | let newTop = e.clientY - startY;
61 |
62 | newTop = Math.min(Math.max(0, newTop), window.innerHeight - button.offsetHeight);
63 |
64 | button.style.top = newTop + "px";
65 | }
66 |
67 | function onMouseUp() {
68 | document.removeEventListener("mousemove", onMouseMove);
69 | document.removeEventListener("mouseup", onMouseUp);
70 | }
71 | });
72 |
73 | button.onclick = function (e) {
74 | const floatingIcon = e.target.closest(".floating-icon");
75 | if (floatingIcon) {
76 | const originalContent = floatingIcon.innerHTML;
77 | floatingIcon.innerHTML = loadingButtonContent;
78 |
79 | processContent().finally(() => {
80 | setTimeout(() => {
81 | floatingIcon.innerHTML = originalContent;
82 | }, 2000);
83 | });
84 | } else {
85 | chrome.runtime.sendMessage({ action: "openSidebar" });
86 | }
87 | };
88 |
89 | document.body.appendChild(button);
90 |
91 | const alertContainer = document.createElement("div");
92 | alertContainer.id = "custom-alert-container";
93 | alertContainer.innerHTML = `
94 |
100 | `;
101 | document.body.appendChild(alertContainer);
102 | };
103 |
104 | if (document.readyState === "complete" || document.readyState === "interactive") {
105 | initializeExtension();
106 | } else {
107 | document.addEventListener("DOMContentLoaded", initializeExtension);
108 | }
109 | })();
110 |
111 | function getUserInput() {
112 | return new Promise((resolve, reject) => {
113 | chrome.storage.local.get(["Super2BrainToken"], (result) => {
114 | if (chrome.runtime.lastError) {
115 | reject(chrome.runtime.lastError);
116 | } else {
117 | resolve(result.Super2BrainToken);
118 | }
119 | });
120 | });
121 | }
122 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Super2Brain - aid your thinking",
4 | "short_name": "S2B - AI agent hub",
5 | "version": "1.0.4",
6 | "description": "不仅网页亮点速览,S2B是一款能自动操控浏览器,让AI批量阅读整理web资料、 OpenAI Deep Research 、DeepSeek R1 的强大浏览器插件。",
7 | "author": "LiM",
8 | "icons": {
9 | "16": "/icons/logo.png",
10 | "32": "/icons/logo.png",
11 | "48": "/icons/logo.png",
12 | "192": "/icons/logo.png"
13 | },
14 | "permissions": [
15 | "storage",
16 | "scripting",
17 | "sidePanel",
18 | "tabs",
19 | "desktopCapture",
20 | "activeTab",
21 | "contextMenus"
22 | ],
23 | "background": {
24 | "service_worker": "background.js",
25 | "type": "module"
26 | },
27 | "content_scripts": [
28 | {
29 | "matches": [
30 | "
"
31 | ],
32 | "js": [
33 | "Readability.js",
34 | "turndown.js",
35 | "inject.js",
36 | "content-script.js"
37 | ],
38 | "css": [
39 | "index.css"
40 | ],
41 | "run_at": "document_end"
42 | }
43 | ],
44 | "host_permissions": [
45 | ""
46 | ],
47 | "action": {
48 | "default_title": "Super2Brain"
49 | },
50 | "side_panel": {
51 | "default_path": "sidepanel.html"
52 | },
53 | "web_accessible_resources": [
54 | {
55 | "resources": [
56 | "index.css",
57 | "welcome.html",
58 | "inject.js"
59 | ],
60 | "matches": [
61 | ""
62 | ]
63 | }
64 | ]
65 | }
--------------------------------------------------------------------------------
/public/markmap.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Mindmap
7 |
121 |
122 |
123 |
124 |
125 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/public/markmap.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("DOMContentLoaded", () => {
2 | const { markmap } = window;
3 | const options = {};
4 | const transformer = new markmap.Transformer();
5 | const svg = document.querySelector(".markmap > svg");
6 | const mm = markmap.Markmap.create(svg, options);
7 | const updateThreshold = 25; // 默认字长变化阈值
8 | const codeUpdateThreshold = 100; // 处于代码块中时更新字符数量触发渲染的阈值
9 | let lastContent = "";
10 | let contentLength = 0;
11 | let inCodeBlock = false; // 标记是否处于代码块内
12 | let lastUpdateTime = 0; // 上次更新时间
13 | const updateInterval = 3000; // 更新间隔(毫秒)
14 |
15 | function removeBackticks(markdown_content) {
16 | if (markdown_content.startsWith("```")) {
17 | markdown_content = markdown_content.split("\n").slice(1).join("\n");
18 | }
19 | if (markdown_content.startsWith("```") && markdown_content.endsWith("```")) {
20 | markdown_content = markdown_content.split("\n").slice(1, -1).join("\n");
21 | }
22 | return markdown_content;
23 | }
24 |
25 | function updateCodeBlockStatus(markdown_content) {
26 | markdown_content = removeBackticks(markdown_content);
27 | const lines = markdown_content.split("\n");
28 | let localInCodeBlock = false;
29 | for (let line of lines) {
30 | if (line.startsWith("```")) {
31 | if (inCodeBlock) {
32 | inCodeBlock = false;
33 | } else {
34 | inCodeBlock = true;
35 | }
36 | }
37 | if (inCodeBlock) {
38 | localInCodeBlock = true;
39 | }
40 | }
41 | inCodeBlock = localInCodeBlock;
42 | }
43 |
44 | function render(markdown_content) {
45 | markdown_content = removeBackticks(markdown_content);
46 | const { root } = transformer.transform(markdown_content);
47 | mm.setData(root);
48 | mm.fit();
49 | }
50 |
51 | chrome.storage.local.get("mindmapContent", (data) => {
52 | const content = data.mindmapContent || "";
53 | document.getElementById("mindmap-content").textContent = content;
54 | lastContent = content;
55 | contentLength = content.length;
56 | updateCodeBlockStatus(content);
57 | render(content);
58 | lastUpdateTime = Date.now();
59 | });
60 |
61 | chrome.runtime.onMessage.addListener((message) => {
62 | if (message.action === "updateContent") {
63 | const newContent = message.content;
64 | document.getElementById("mindmap-content").textContent = newContent;
65 |
66 | updateCodeBlockStatus(newContent); // 更新代码块状态
67 |
68 | let charUpdateThreshold = inCodeBlock ? codeUpdateThreshold : updateThreshold;
69 |
70 | const currentTime = Date.now();
71 | if (
72 | Math.abs(newContent.length - contentLength) >= charUpdateThreshold ||
73 | currentTime - lastUpdateTime > updateInterval
74 | ) {
75 | render(newContent);
76 | lastContent = newContent;
77 | contentLength = newContent.length;
78 | lastUpdateTime = currentTime; // 更新最后更新时间
79 | }
80 | }
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/public/utils.js:
--------------------------------------------------------------------------------
1 | export const keepAlive = (() => {
2 | let intervalId;
3 |
4 | return async (state) => {
5 | if (state && !intervalId) {
6 | chrome.runtime.getPlatformInfo(() => {});
7 | intervalId = setInterval(() => chrome.runtime.getPlatformInfo(() => {}), 20000);
8 | } else if (!state && intervalId) {
9 | clearInterval(intervalId);
10 | intervalId = null;
11 | }
12 | };
13 | })();
14 |
15 | export async function captureVisibleTab() {
16 | try {
17 | const [tab] = await chrome.tabs.query({
18 | active: true,
19 | currentWindow: true,
20 | });
21 |
22 | // 捕获可见区域的截图
23 | const dataUrl = await chrome.tabs.captureVisibleTab(null, {
24 | format: "png",
25 | quality: 100,
26 | });
27 |
28 | return {
29 | success: true,
30 | dataUrl,
31 | title: tab.title,
32 | url: tab.url,
33 | };
34 | } catch (error) {
35 | console.error("截图失败:", error);
36 | return {
37 | success: false,
38 | error: error.message,
39 | };
40 | }
41 | }
42 |
43 | // 可选:如果需要完整页面截图
44 | export async function captureFullPage() {
45 | try {
46 | const [tab] = await chrome.tabs.query({
47 | active: true,
48 | currentWindow: true,
49 | });
50 |
51 | // 注入脚本获取页面完整高度
52 | const [{ result }] = await chrome.scripting.executeScript({
53 | target: { tabId: tab.id },
54 | function: () => {
55 | return {
56 | width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth),
57 | height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
58 | };
59 | },
60 | });
61 |
62 | // 调整标签页大小
63 | await chrome.tabs.update(tab.id, { url: tab.url });
64 | await new Promise((resolve) => setTimeout(resolve, 1000));
65 |
66 | // 捕获截图
67 | const dataUrl = await chrome.tabs.captureVisibleTab(null, {
68 | format: "png",
69 | quality: 100,
70 | });
71 |
72 | return {
73 | success: true,
74 | dataUrl,
75 | title: tab.title,
76 | url: tab.url,
77 | };
78 | } catch (error) {
79 | console.error("完整页面截图失败:", error);
80 | return {
81 | success: false,
82 | error: error.message,
83 | };
84 | }
85 | }
86 |
87 | export async function initializeScreenCapture() {
88 | try {
89 | // 获取屏幕媒体流
90 | const stream = await navigator.mediaDevices.getDisplayMedia({
91 | video: {
92 | cursor: "always",
93 | },
94 | audio: false,
95 | });
96 |
97 | // 创建视频元素
98 | const video = document.createElement("video");
99 | video.srcObject = stream;
100 |
101 | // 等待视频加载
102 | await new Promise((resolve) => (video.onloadedmetadata = resolve));
103 | await video.play();
104 |
105 | // 创建画布
106 | const canvas = document.createElement("canvas");
107 | canvas.width = video.videoWidth;
108 | canvas.height = video.videoHeight;
109 |
110 | // 绘制视频帧
111 | const ctx = canvas.getContext("2d");
112 | ctx.drawImage(video, 0, 0);
113 |
114 | // 停止所有轨道
115 | stream.getTracks().forEach((track) => track.stop());
116 |
117 | // 转换为图片数据
118 | const dataUrl = canvas.toDataURL("image/png");
119 |
120 | return {
121 | success: true,
122 | dataUrl,
123 | width: canvas.width,
124 | height: canvas.height,
125 | };
126 | } catch (error) {
127 | console.error("区域截图失败:", error);
128 | return {
129 | success: false,
130 | error: error.message,
131 | };
132 | }
133 | }
134 |
135 | // 将 base64 图片数据转换为 Markdown 图片格式
136 | export function convertToMarkdownImage(dataUrl, alt = "截图") {
137 | return ``;
138 | }
139 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## Super2Brain 浏览器插件
2 |
3 | ### 主要功能特性
4 |
5 | - 网页长文章速览
6 | - 自动操控浏览器访问多标签页,生成洞察分析报告
7 | - 与当前标签页内容对话
8 | - 通过操作浏览器多轮后生成最后的报告
9 |
10 |
11 |
12 |
13 |
14 | ### 其它功能特性
15 |
16 | - 🤖 多模型 AI 支持
17 |
18 | - DeepSeek API 集成
19 | - OpenAI API 集成
20 | - 灵活的模型切换
21 |
22 | - 📊 智能内容分析
23 |
24 | - 网页内容智能总结
25 | - 关键信息自动提取
26 | - 智能摘要生成
27 | - 思维导图可视化
28 |
29 | - 🔒 数据安全与隐私
30 | - 本地数据存储保护
31 | - 用户隐私保障
32 | - 便捷导入知识库
33 |
34 | ### 技术栈
35 |
36 | - Next.js
37 | - React
38 | - TailwindCSS
39 | - Markmap
40 |
41 | ### 开始使用
42 |
43 | 1. 克隆项目
44 |
45 | ```bash
46 | git clone https://github.com/your-username/Super2Brain.git
47 |
48 | cd Super2Brain
49 | ```
50 |
51 | 2. 安装依赖
52 |
53 | ```bash
54 | npm install
55 | ```
56 |
57 | 3. 编译
58 |
59 | ```bash
60 | npm run prep
61 | ```
62 |
63 | ### 系统要求
64 |
65 | - Node.js 16.x 或更高版本
66 | - npm 7.x 或更高版本
67 |
68 | ### 许可证
69 |
70 | ISC License
71 |
72 | ### 贡献指南
73 |
74 | 欢迎提交 Issue 和 Pull Request 来帮助改进项目。
75 |
76 | ### 感谢与开源声明
77 |
78 | 本项目基于以下优秀的开源项目构建:
79 |
80 | - [Markmap](https://markmap.js.org/) - 用于生成思维导图的开源库 (MIT License)
81 | - [Next.js](https://nextjs.org/) - React 框架 (MIT License)
82 | - [React](https://reactjs.org/) - 用户界面库 (MIT License)
83 | - [TailwindCSS](https://tailwindcss.com/) - CSS 框架 (MIT License)
84 | - [memfree](https://github.com/memfreeme/memfree) - 用于生成思维导图的开源库 (MIT License)
85 |
86 | 特别感谢以上项目的贡献者们。本项目遵循 ISC License,详细许可证信息请查看 [LICENSE](./LICENSE) 文件。
87 |
88 | 如果您使用了本项目,请保留相关的版权信息和许可证声明。
89 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .text-gradient_indigo-purple {
6 | background: linear-gradient(90deg, #6366f1 0%, rgb(168 85 247 / 0.8) 100%);
7 | -webkit-background-clip: text;
8 | -webkit-text-fill-color: transparent;
9 | background-clip: text;
10 | }
11 |
12 | :root {
13 | color-scheme: light;
14 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": false,
11 | "noEmit": true,
12 | "incremental": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve"
19 | },
20 | "include": [
21 | "next-env.d.ts",
22 | "**/*.ts",
23 | "**/*.tsx"
24 | ],
25 | "exclude": [
26 | "node_modules"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------