Greasy Fork is available in English.

ViSearch/Google of Google

Beyond the ChatGPT/AI with eyes Now:ViSearch on Chrome extensions https://chrome.google.com/webstore/search/visearch

// ==UserScript==
// @name         ViSearch/Google of Google
// @name:zh-CN    ViSearch/Google of Google
// @name:zh-TW    ViSearch/Google of Google
// @name:fr    ViSearch/Google of Google
// @name:es    ViSearch/Google of Google
// @name:th    ViSearch/Google of Google
// @namespace    https://github.com/new4u
// @version      7.117.2
// @description  Now on Chrome extensions!!! **https://chrome.google.com/webstore/search/visearch** ! Beyond the ChatGPT/AI with eyes.ViSearch as the Free/Lightweight alternatives to ChatGPT,has the potential to be even more intuitive than ChatGPT in the future.
// @description:zh-cn  Beyond the ChatGPT/AI with eyes Now:ViSearch on Chrome extensions https://chrome.google.com/webstore/search/visearch
// @description:zh-tw  Beyond the ChatGPT/AI with eyes Now:ViSearch on Chrome extensions https://chrome.google.com/webstore/search/visearch
// @description:fr  Beyond the ChatGPT/AI with eyes Now:ViSearch on Chrome extensions https://chrome.google.com/webstore/search/visearch
// @description:es  Beyond the ChatGPT/AI with eyes Now:ViSearch on Chrome extensions https://chrome.google.com/webstore/search/visearch
// @description:th  Beyond the ChatGPT/AI with eyes Now:ViSearch on Chrome extensions https://chrome.google.com/webstore/search/visearch
// @author       new4u本爷有空
// @connect    google.com
// @connect    google.com.hk
// @connect    google.com.jp
// @include    *://encrypted.google.*/search*
// @include    *://*.google*/search*
// @include    *://*.google*/webhp*
// @match        *www.google.com*
// @icon        https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/WikiProject_Sociology_Babel_%28Deus_WikiProjects%29.png/240px-WikiProject_Sociology_Babel_%28Deus_WikiProjects%29.png
// @require     https://unpkg.com/d3@4.13.0/build/d3.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js
// @require     http://cdn.bootcss.com/jquery/2.1.4/jquery.min.js
// @require     http://cdn.bootcss.com/bootstrap/3.3.4/js/bootstrap.min.js
// @resource    http://cdn.bootcss.com/bootstrap/3.3.4/css/bootstrap.min.css
// @grant        none
// @copyright    2015-2023, new4u
// @license      GPL-3.0-only
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw
// @note         2023-05-25-v.7.117 发布到了chrome应用商店,并且修复了一个关键词和chrome界面语种的冲突,支持多语种.
// @note         2023-2-21-v1.117 发布迁移到userscript以来第一个正式版本
// @note         2016 Java -> 2019 Node.js -> 2023-2-09 各种各样的历史更新记录,从一个版本迭代到另一个版本
// @grant        none
// ==/UserScript==

let styleSheet = `
body {
    padding: 30px 40px;
    font-family: OpenSans-Light, PingFang SC, Hiragino Sans GB, Microsoft Yahei, Microsoft Jhenghei, sans-serif;
}

.links line {
    stroke: rgb(255, 255, 255);
    stroke-opacity: 0.5;
}

.links line.inactive {
    stroke-opacity: 0;
}

.nodes circle {
    stroke: #fff;
    stroke-width: 1.5px;
}

.nodes circle:hover {
    cursor: pointer;
}

.nodes circle.inactive {
    display: none !important;
}


@media screen and (max-width: 600px) {
    .text {
      font-size: 8px; /* 当屏幕宽度小于600px时 最小字体为8px */
    }
  }

  .texts text {
    font-size: 12px; /* 最小字体为12px */
    min-font-size: 8px; /* 最小字体不能小于8px */
    max-font-size: 36px;
    font-weight:bold;
    font-family:"Microsoft YaHei";
    text-shadow: 0 0 3px #fff, 0 0 10px #fff;
  }



.texts text:hover {
    cursor: pointer;
}

.texts text.inactive {
    display: none !important;
}


#mode {
    position: absolute;
    top: 160px;
    left: 60px;
}

#mode span {
    display: inline-block;
    border: 1px solid #fff;
    color: #fff;
    padding: 6px 10px;
    border-radius: 4px;
    font-size: 14px;
    transition: color, background-color .3s;
    -o-transition: color, background-color .3s;
    -ms-transition: color, background-color .3s;
    -moz-transition: color, background-color .3s;
    -webkit-transition: color, background-color .3s;
}



#info {
    position: absolute;
    bottom: 40px;
    right: 30px;
    text-align: right;
    width: 270px;
}

#info h4 {
    color: #fff;
}

#info p {
    color: #fff;
    font-size: 12px;
    margin-bottom: 5px;
}

#info p span {
    color: #888;
    margin-right: 10px;
}

#svg g.row:hover {
    stroke-width: 1px;
    stroke: #fff;
}
`;

























let s = document.createElement('style');
s.type = "text/css";
s.innerHTML = styleSheet;
(document.head || document.documentElement).appendChild(s);

const colors = ["#6ca46c", "#4e88af", "#c72eca", "#d2907c"];
//临时半径R大小控制,之后改成连接数量影响大小
const sizes = [30, 20, 20, 0.5];
const forceRate = -1000;
// 选择 input 元素中 class 为 gLFyf 的最后一个元素
const input = document.querySelector("input.gLFyf:last-of-type");
let searchtext = input ? input.value : "";
if (searchtext === "") {
    console.log("searchtext is empty");
} else {
    console.log("searchtext:", searchtext);
    // 执行其他逻辑
}












function parseWebPage(Searchtext) {
    //data
    //***开始解析网页
    /* 谷歌获取 */

    //成功在page之外获取到页面元素内容,发现百度的下一页,就是
    let elements = Array.from(document.querySelectorAll(".g"));
    // console.log(elements);
    //读取数组里内容map为value
    let dataPage = elements.map((element) => {
        // console.log(element);

        //搜索到文章的标题
        let title = element.querySelector(".LC20lb");
        title !== null ? (title = title.innerText) : (title = null);

        // console.log(title);

        //搜索到文章的url
        let url = element.querySelector("a");
        url !== null ? (url = url.href) : (url = null);

        // console.log(url);

        //搜索到的文章的来源网站r.split(" - ")[1]
        let siteName = title;
        siteName !== null
            ? (siteName = siteName.split(" - ")[1])
        : (siteName = null);
        // console.log(siteName);

        //搜索到的文章的发布日期
        // let time = element.querySelector(".c-abstract>.newTimeFactor_before_abs"); //之前的query
        let time = element.querySelector(".MUxGbd.wuQ4Ob.WZ8Tjf > span");
        time !== null ? (time = time.innerText) : (time = null); //google几天前时间可以计算一下

        // console.log(time);

        //搜索到的文章的摘要
        let abstract = element.querySelector(
            ".VwiC3b.yXK7lf.MUxGbd.yDYNvb.lyLwlc.lEBKkf span:nth-child(2)"
        );
        abstract !== null ? (abstract = abstract.innerText) : (abstract = null);

        //搜索到关键词如果2023年05月25日更新到多语种
        let keyWords = Array.from(element.querySelectorAll("em"));
        keyWords !== null
            ? (keyWords = keyWords.map((item) => {
            return item.innerText;
        }))
        : (keyWords = null);
        // console.log(keyWords);

        let elementEach = {
            title,
            url,
            siteName,
            time,
            abstract,
            keyWords,
        };

        // console.log(elementEach);
        return elementEach;
    });

    console.log(dataPage);

    let keyWords = [];
    for (let i in dataPage) {
        let temp = dataPage[i].keyWords;
        keyWords = keyWords.concat(temp);
    }
    let keyWordsSet = Array.from(new Set(keyWords));

    let nodes = [];
    let links = [];
    let id = 0;

    let sanidstart = id;
    let sanNode = [];

    for (let j = 0; j < dataPage.length; j++) {
        sanNode.push({
            category: 4,
            id: "san" + j,
            name: dataPage[j].title,
            value: dataPage[j].abstract,
            origin: dataPage[j].siteName,
            time: dataPage[j].time,

            year:
            dataPage[j].time !== null &&
            dataPage[j].time.replace(/[^0-9\u4e00-\u9fa5]/gi, "") !== ""
            ? dataPage[j].time.replace(/[^0-9\u4e00-\u9fa5]/gi, "").substr(0, 4)
            : null,
            url: dataPage[j].url,
            //为了前面能找到,再加一条,也可以用这个循环,就少了一个!,这样资料全一些
            keyWords: dataPage[j].keyWords,
            type: "san",
        });
    }
    let keyidstart = id;
    let keyNode = [];
    for (let i = 0; i < keyWordsSet.length; i++) {
        if (keyWordsSet[i].length <= 4) {
            keyNode.push({
                category: 3,
                id: "key" + i,
                name: keyWordsSet[i],
                value: 30000 + i + "",
                type: "key",
            });
            //     !!! link 如果 keyNode.name 在dataPage.keywords li indexof 就连接(先写san)
            //    for (k)
            for (let j = 0; j < sanNode.length; j++) {
                //keywords是数组,不知道行不行,可以
                if (sanNode[j].keyWords.indexOf(keyWordsSet[i]) !== -1) {
                    links.push({
                        source: "key" + i,
                        target: "san" + j,
                        value: 1,
                    });
                }
            }
        }
    }

    let tagid = id;
    //2,tag,id=20000-29999,tag就是长度大于4的keywordsSet
    let tagNode = [];
    for (let i = 0; i < keyWordsSet.length; i++) {
        if (keyWordsSet[i].length > 4) {
            tagNode.push({
                category: 2,
                id: "tag" + i,
                type: "tag",
                name: keyWordsSet[i],
                value: id,
            });
            for (let j = 0; j < sanNode.length; j++) {
                //keywords是数组,不知道行不行,可以
                if (sanNode[j].keyWords.indexOf(keyWordsSet[i]) !== -1) {
                    links.push({
                        source: "tag" + i,
                        target: "san" + j,
                        value: 1,
                    });
                }
            }

            for (let j = 0; j < keyNode.length; j++) {
                //如果tagNode.name包含了keyNode.name(indexof),就连一条线
                if (keyWordsSet[i].indexOf(keyNode[j].name) !== -1) {
                    links.push({
                        source: "tag" + i,
                        target: keyNode[j].id,
                        value: 1,
                    });
                }
            }
        }
    }

    //1,id=10000,不能用10000+"",,只能用i+"".直接"10000"就好了
    let newsNode = {
        category: 1,
        id: "news",
        //searchtext在前面定义前面获取
        name: searchtext,
        value: id,
        type: "news",
    };
    //和key和tag都建立连接
    for (let i = 0; i < keyNode.length; i++) {
        links.push({
            source: "news",
            target: keyNode[i].id,
            value: 1,
        });
    }
    for (let i = 0; i < tagNode.length; i++) {
        links.push({
            source: "news",
            target: tagNode[i].id,
            value: 1,
        });
    }

    //    合并4个[],用concat(),
    nodes = nodes
        .concat(newsNode)
        .concat(tagNode)
        .concat(keyNode)
        .concat(sanNode);

    //data end

    return {
        nodes,
        links,
    };
}



















function renderD3Graph(nodes, links, graph) {
    // 获取 graph 元素的宽度和高度
    const width = parseInt(graph.style.width);
    const height = parseInt(graph.style.height);

    d3.selectAll("graph > *").remove();

    // // 创建 D3.js SVG 元素,并设置其大小和位置
    const bbox = graph.getBoundingClientRect();

    const svg = d3.select(graph).append("svg")
    .attr("id","graph-svg")
    .attr("width", bbox.width)
    .attr("height", bbox.height)
    .style("position", "fixed")
    .style("top", bbox.y + "px")
    .style("left", bbox.x + "px")
    .style("z-index", "999");


    //   const svg = d3
    // .select(graph)
    // .append("svg")
    // .attr("width", width)
    // .attr("height", height)
    // .style("position", "fixed")
    // .style("top", "0")
    // .style("right", "0")
    // .style("z-index", "999");

    function dragStarted(d) {
        if (!d3.event.active) simulation.alphaTarget(0.3).restart();
        d.fx = d.x;
        d.fy = d.y;
    }

    function dragging(d) {
        d.fx = d3.event.x;
        d.fy = d3.event.y;
    }

    function dragEnded(d) {
        if (!d3.event.active) simulation.alphaTarget(0);
        d.fx = null;
        d.fy = null;
    }

    const simulation = d3
    .forceSimulation(nodes)
    .force(
        "link",
        d3
        .forceLink(links)
        .id((d) => d.id)
        .distance(50)
        .strength(1)
        .iterations(1)
    )
    .force("charge", d3.forceManyBody().strength(forceRate))
    .force("center", d3.forceCenter(width / 2, height / 2))
    .alphaDecay(0.05);

    const link = svg
    .append("g")
    .attr("stroke", "#999")
    .attr("stroke-opacity", 0.6)
    .selectAll("line")
    .data(links)
    .enter()
    .append("line")
    .attr("stroke-width", (d) => Math.sqrt(d.value));

    const node = svg
    .append("g")
    .selectAll("circle")
    .data(nodes)
    .enter()
    .append("circle")
    //        试着给每种type加一个class,要放在数据读取之后
    .attr("class", function (d) {
        // return "nodes";
        return "nodes " + d.type;
    })
    .attr("r", function (d) {
        //    make radius of node circle
        return sizes[d.category - 1]; // return r;
    })
    .attr("fill", function (d) {
        //    配合现在category从1开始,今后可以重新设计一下category make color of node circle,也可以加到后面,统一修改
        // console.log(d.category);
        return colors[d.category - 1];
    })
    .attr("stroke", "none")
    .attr("name", function (d) {
        //    make text of node
        return d.name;
    })

    .call(
        d3
        .drag()
        .on("start", dragStarted)
        .on("drag", dragging)
        .on("end", dragEnded)
    );

    //固定中心文章位置,用class来控制
    //固定中心文章位置,fx可以设置哈哈,或者大面积的可以用tick,详见https://stackoverflow.com/questions/10392505/fix-node-position-in-d3-force-directed-layout,实验证明,还可以用type属性来控制fx,fy

    svg
        .select(".news")
        .attr("fx", function (d) {
        return (d.fx = width / 2);
    })
        .attr("fy", function (d) {
        return (d.fy = height / 2);
    });

    //san文章点击, 没有name这个
    //存放三度文章size
    svg
        .selectAll(".san")
        .attr("r", function (d) {
        let uniqueWords = new Set(d.keyWords);
        let radius = uniqueWords.size * 10;
        // console.log("radius:", radius);
        return radius;
    })
        .style("fill-opacity", 0.5);

    //    显示所有的文本...
    const text = svg
    .append("g")
    .attr("class", "texts")
    .selectAll("text")
    .data(nodes)
    .enter()
    .append("text")
    .attr("font-size", function (d) {
        // return d.size;
        let uniqueWords = new Set(d.keyWords);
        let radius = uniqueWords.size * 2;
        let fontSize = radius * sizes[d.category - 1];

        // console.log("d:", d, ";font-size return", fontSize);
        return fontSize;
    })
    .attr("fill", function (d) {
        // return "red";
        return colors[d.category - 1];
    })
    .attr("name", function (d) {
        return d.time;
    })
    .text(function (d) {
        return d.time ? d.time + d.name : d.name;
    })
    .attr("text-anchor", "center")
    .on("click", function (d) {
        if (d.url) {
            window.open(d.url, "_blank");
        }
    })
    .call(
        d3
        .drag()
        .on("start", dragStarted)
        .on("drag", dragging)
        .on("end", dragEnded)
    );

    //圆增加title...
    node.append("title").text(function (d) {
        return d.time + d.name;
    });


    /* //点击任意一个node, 不与之相连的节点和连线都变透明,怎么做

    可以在点击node事件处理函数中通过改变对应元素的透明度实现:

    获取点击的node的相邻节点
    对于不相邻的节点,修改其透明度
    对于不相邻的连线,修改其透明度
    代码示例: */

    var isTransparent = false;

    // 为每个node绑定点击事件
    node.on("click", function(d) {
        // 根据当前状态进行相应的操作
        if (!isTransparent) {
            link.style("opacity", function(l) {
                if (d === l.source || d === l.target) {
                    return 1;
                } else {
                    return 0.1;
                }
            });
            node.style("opacity", function(n) {
                // 只对与点击的圆圈不相关的圆圈透明度进行更改
                var linked = false;
                link.each(function(l) {
                    if (d === l.source || d === l.target) {
                        linked = true;
                        return;
                    }
                });
                if (!linked) {
                    return 0.1;
                } else {
                    return 1;
                }
            });
            isTransparent = true;
        } else {
            link.style("opacity", 1);
            node.style("opacity", 1);
            isTransparent = false;
        }
    });

    simulation.on("tick", () => {
        link
            .attr("x1", (d) => d.source.x)
            .attr("y1", (d) => d.source.y)
            .attr("x2", (d) => d.target.x)
            .attr("y2", (d) => d.target.y);

        node.attr("cx", (d) => d.x).attr("cy", (d) => d.y);
        text.attr("transform", function (d) {
            // return 'translate(' + d.x + ',' + (d.y + d.size / 2) + ')';
            return "translate(" + d.x + "," + (d.y + sizes[d.category - 1] / 2) + ")";
        });
    });
}































(function () {
    let button = document.createElement("button");
    button.innerHTML = "ViSearch";
    button.style.cssText =
        "position: fixed; top: 0; right: 0; z-index: 999; width: 100px; height: 50px; background-color: green; color: white; font-size: 20px;";

    // Add button to page
    document.body.appendChild(button);


    // Create graph element
    const graph = document.createElement("div");
    graph.style.position = "fixed"
    graph.style.top = "50px"
    graph.style.right = "0px"
    graph.style.width = "500px"
    graph.style.height = "500px"
    graph.style.background = "#fff"
    graph.style.border = "1px solid #ccc"
    graph.style.display = "none"
    graph.style.opacity = 0.9;


    // Add graph to page
    document.body.appendChild(graph);
    let graphVisible = false;

    // Toggle graph on button click
    button.addEventListener("click", () => {
        if (graphVisible) {
            graph.style.display = "none";
            graphVisible = false;
        } else {
            graph.style.display = "block";
            graphVisible = true;
            data = parseWebPage(searchtext);
            console.log("data", data);
            nodes = data.nodes;
            links = data.links;
            //remove the previous canvs
            d3.select("#graph-svg").remove();
            console.log("#graph-svg", d3.select("#graph-svg"));


            renderD3Graph(nodes, links, graph);
        }
    });

    // Hide graph on outside click
    document.addEventListener("click", (event) => {
        if (
            graphVisible &&
            event.target !== graph &&
            !graph.contains(event.target) &&
            event.target !== button
        ) {
            graph.style.display = "none";
            graphVisible = false;
        }
    });
})();