V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
zapll
V2EX  ›  程序员

使用 ChatGPT 突破能力边界 之 前端小白实现沉浸式翻译的核心功能

  •  
  •   zapll · 354 天前 · 1907 次点击
    这是一个创建于 354 天前的主题,其中的信息可能已经有所发展或是发生改变。

    作为一个后端开发, 一直比较好奇沉浸式翻译的效果是如何实现的, 所以咱也当了回甲方, 让 ChatGPT 帮我实现想要的效果

    场景

    为了锻炼英文阅读能力, 在英文文章阅读时还是期望能多读外文, 如果一把梭翻译一遍, 普遍是只读中文不看英文了, 那么也就起不到我们锻炼英文能力的要求了.

    需求

    如果我遇到了一些不认识或者不确定意思的单词,我希望得到一个贴合当前语境的翻译,这样既能理解文章,又能在语境中学习单词。

    需求拆解

    如果鼠标在某个单词上方悬浮超过 xx 毫秒, 那么则结合当前语境翻译这个单词及这个句子.

    好的, 需求有了, 思路也有了, 具体咋做咱就不知道了, 现在轮到我最强的小弟 ChatGPT 大显身手了

    效果图

    image.png

    具体功能点:

    1. 关键词的翻译, 译文紧跟在关键词后面
    2. 当前语句的翻译

    实现此效果的完整 ChatGPT 对话: 实现沉浸式翻译的核心功能

    插播广告:

    推荐一个 ChatGPT Plus 拼车服务, 客服wx, 无需科学上网/会话隔离

    最终完整代码

    import type { PlasmoCSConfig } from "plasmo"
    import { generate } from 'short-uuid'
    
    export const config: PlasmoCSConfig = {
        matches: ["<all_urls>"],
        all_frames: true
    }
    
    const loading = `
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><radialGradient id="a11" cx=".66" fx=".66" cy=".3125" fy=".3125" gradientTransform="scale(1.5)"><stop offset="0" stop-color="#85A2B6"></stop><stop offset=".3" stop-color="#85A2B6" stop-opacity=".9"></stop><stop offset=".6" stop-color="#85A2B6" stop-opacity=".6"></stop><stop offset=".8" stop-color="#85A2B6" stop-opacity=".3"></stop><stop offset="1" stop-color="#85A2B6" stop-opacity="0"></stop></radialGradient><circle transform-origin="center" fill="none" stroke="url(#a11)" stroke-width="15" stroke-linecap="round" stroke-dasharray="200 1000" stroke-dashoffset="0" cx="100" cy="100" r="70"><animateTransform type="rotate" attributeName="transform" calcMode="spline" dur="2" values="360;0" keyTimes="0;1" keySplines="0 0 1 1" repeatCount="indefinite"></animateTransform></circle><circle transform-origin="center" fill="none" opacity=".2" stroke="#85A2B6" stroke-width="15" stroke-linecap="round" cx="100" cy="100" r="70"></circle></svg>
    `
    const cssCode = `
    .ct-loading {
        width: 1em;
        display: inline-block;
    }
    
    .ct-span {
        display: inline-block;
        margin-right: 2px;
        margin-left: 2px;
    }
    `
    
    var xy = { x: 0, y: 0 }
    
    document.head.insertAdjacentHTML('beforeend', '<style type="text/css">' + cssCode + '</style>');
    // 存储已经翻译过的单词,以避免重复翻译
    var translatedWords = new Set(); // 用于存储已翻译的单词及其父元素
    var pool = {}
    
    document.addEventListener('mousemove', function (event) {
        // 获取鼠标位置
        var x = event.clientX, y = event.clientY;
        xy = { x, y }
    
        // 获取鼠标位置下的文本范围
        var range, textNode, offset;
        range = document.caretRangeFromPoint(x, y);
        textNode = range.startContainer;
        if (!textNode) {
            return
        }
        if (textNode.parentElement.hasAttribute('data-ct-translation')) { // 翻译结果节点
            return;
        }
        if (hasPreOrCodeParent(textNode)) {
            return
        }
        offset = range.startOffset;
    
        var text = textNode.textContent;
        var words = text.split(/\s+/);
        for (var i = 0; i < words.length; i++) {
            var word = removeSpecialCharacters(words[i]);
            if (!isEnglishWord(word)) {
                continue;
            }
            // 检测鼠标是否在当前单词上
            var wordRange = document.createRange();
            wordRange.setStart(textNode, text.indexOf(word));
            wordRange.setEnd(textNode, text.indexOf(word) + word.length);
            var rects = wordRange.getClientRects();
            for (var j = 0; j < rects.length; j++) {
                var rect = rects[j];
                if (!(x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom)) {
                    continue;
                }
                var parentElementId = textNode.parentElement.getAttribute('data-ct-id');
                if (parentElementId) {
                    translatedWords[word] = parentElementId;
                } else {
                    parentElementId = generate();
                    textNode.parentElement.setAttribute('data-ct-id', parentElementId);
                }
    
                if (translatedWords.has(word + '-' + parentElementId)) {
                    continue;
                }
    
                // 创建包装的 span 元素
                // console.log();
                var spanElement = document.createElement('span');
                const wordId = generate()
                spanElement.setAttribute('id', wordId);
                spanElement.setAttribute('data-pid', parentElementId);
                spanElement.setAttribute('data-ct-translation', '0');
                spanElement.setAttribute('data-ct-word', word);
                spanElement.setAttribute('data-ct-rect', `${rect.left},${rect.right},${rect.top},${rect.bottom}`);
                spanElement.setAttribute('data-ct-parent', getFullSentenceContainingWord(textNode.parentElement));
                wordRange.collapse(false); // 将光标移到范围的末尾
                wordRange.insertNode(spanElement);
                translatedWords.add(word + '-' + parentElementId);
                pool[wordId] = {}
                // console.log('Word under mouse:', word);
            }
        }
    });
    
    setInterval(() => {
        for (const wordId in pool) {
            const wordElement = document.getElementById(wordId);
            if (!wordElement) {
                continue;
            }
            const rect = wordElement.getAttribute('data-ct-rect').split(',').map(Number);
            const word = wordElement.getAttribute('data-ct-word');
            const pid = wordElement.getAttribute('data-pid');
            if (!isMouseOnTranslation({ left: rect[0], right: rect[1], top: rect[2], bottom: rect[3] }, xy.x, xy.y)) {
                translatedWords.delete(word + '-' + pid)
                wordElement.remove()
                delete pool[wordId]
                continue
            }
            wordElement.classList.add('ct-loading')
            wordElement.innerHTML = loading
    
            fetch('http://localhost:8787/translate', {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({
                    word: word,
                    sentence: wordElement.getAttribute('data-ct-parent')
                })
            }).then(res => res.json()).then(res => {
                console.log(res)
                wordElement.classList.remove('ct-loading')
                wordElement.classList.add('ct-span')
                wordElement.innerHTML = `(${res.word_result})`
                const p = document.querySelector(`[data-ct-id="${pid}"]`)
                const cp = p.cloneNode(true)
                cp.textContent = res.sentence_result
                p.after(cp)
            }).catch(e => console.log)
    
            wordElement.setAttribute('data-ct-translation', '1');
            delete pool[wordId]
        }
    }, 1500)
    
    // 判断鼠标是否在翻译内容上
    function isMouseOnTranslation(rect, mouseX, mouseY) {
        return rect && mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom
    }
    
    function isEnglishWord(word) {
        // 使用正则表达式匹配英文单词,这里假设一个英文单词由大小写字母组成
        var regex = /^[a-zA-Z0-9]+$/;
        return regex.test(removePunctuation(word));
    }
    
    
    function removePunctuation(word) {
        // 使用正则表达式匹配标点符号并替换为空字符串
        return word.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '');
    }
    
    function getFullSentenceContainingWord(parentNode) {
        // const tokens = parentNode.innerText.replace(/\n/, '').split('.');
        // return tokens[0];
        return parentNode.innerText.replace(/\n/, '')
    }
    
    function removeSpecialCharacters(str) {
        return str.replace(/[^a-zA-Z0-9]/g, '');
    }
    
    function hasPreOrCodeParent(node) {
        let parent = node.parentNode;
    
        while (parent) {
            if (parent.tagName === 'PRE' || parent.tagName === 'CODE') {
                return true;
            }
            parent = parent.parentNode;
        }
    
        return false;
    }
    
    14 条回复    2024-01-06 12:33:04 +08:00
    w2er
        1
    w2er  
       354 天前
    很棒,谢谢分享,在用一个类似的插件
    vipfts
        2
    vipfts  
       354 天前
    我倒希望有那种点一下单词然后详细翻译单词的沉浸式翻译
    0o0O0o0O0o
        3
    0o0O0o0O0o  
       354 天前 via iPhone   ❤️ 2
    虽然是推广,但是比没有内容的推广好很多,建议多来点
    zapll
        4
    zapll  
    OP
       354 天前
    @vipfts #2 这个触发方式跟悬浮类似, 应该也不难, 可以扔个 ChatGPT 让他改改, 😄
    zapll
        5
    zapll  
    OP
       354 天前
    @0o0O0o0O0o 😄感谢认可, 后续多分享一些我日常使用 ChatGPT 的体验, 在创造性工作上, 帮助我们突破一下现在的能力边界还是蛮有用的
    Sniper000
        6
    Sniper000  
       354 天前
    现在 mac 上用的比较香的有三个,1 ,bob 翻译 option+s 直接截图翻译类似
    2. 这个也相当好用 选中文字后,cmd+cc 多点击一下 C ,就呼出来了。
    SayHelloHi
        7
    SayHelloHi  
       353 天前
    @zapll

    楼主 有 GitHub 项目地址么 😁
    这还得用 plasmo 创建一个开发插件项目 把代码放进去
    sma210
        8
    sma210  
       353 天前
    @Sniper000 第二款有 URL 的不
    unco020511
        9
    unco020511  
       353 天前
    GPT 特别擅长编写像浏览器插件、IDEA 插件这种小型应用程序,事实上,在我完全没有基础的情况下也曾让 GPT 帮助我创建了一个 IDEA 插件。
    Sniper000
        10
    Sniper000  
       353 天前
    @sma210 好像没有
    zapll
        11
    zapll  
    OP
       353 天前
    @Sniper000 这几个工具好用嘞
    @SayHelloHi 没有建 github 仓库, 代码量比较小, create plasmo 然后 代码 贴到 context.ts 里就行了
    Sniper000
        12
    Sniper000  
       353 天前
    @zapll #11 哈哈哈 确实是好用 用习惯了 只能说离不开了已经
    ludage
        13
    ludage  
       352 天前
    请多来一些跟 gpt 的对话,我学习一下,谢谢大佬
    zapll
        14
    zapll  
    OP
       351 天前
    @ludage 我努力💪
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1397 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 16:55 · PVG 00:55 · LAX 08:55 · JFK 11:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.