Greasy Fork

Greasy Fork is available in English.

Claude helper (对话导出\字数统计\时间显示\模型信息显示)

✴️1、可以导出 claude ai当前对话的内容。✴️2、统计当前字数 (包括粘贴、上传、article的内容,含换行符/markdown语法符号等)。✴️3、显示对话的时间。✴️4、显示对话的模型信息。ℹ️显示的信息均来自网页内本身存在但未显示的属性值。

当前为 2024-08-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Claude helper (对话导出\字数统计\时间显示\模型信息显示)
// @name:zh-CN  Claude 助手 (对话导出\字数统计\时间显示\模型信息显示)
// @version      0.5.6
// @description  ✴️1、可以导出 claude ai当前对话的内容。✴️2、统计当前字数 (包括粘贴、上传、article的内容,含换行符/markdown语法符号等)。✴️3、显示对话的时间。✴️4、显示对话的模型信息。ℹ️显示的信息均来自网页内本身存在但未显示的属性值。
// @author       Yearly
// @match        https://claude.ai/*
// @include      https://*claude*.com/*
// @match        https://chat.kelaode.ai/*
// @icon         data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgODAgODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgMGg4MHY4MEgweiIgZmlsbD0iIzQ0NSIvPjxwYXRoIGQ9Im0zMyA0NC0yMy0xYy0xIDAtMi0yLTItM3MwLTEgMS0xbDI0IDItMjEtMTVjMC0xLTEtMS0xLTNzMy00IDYtMmwxNCAxMi05LTE3di0yYzAtMSAxLTUgMy01IDEgMCAzIDAgNCAxbDExIDIzIDItMjBjMC0yIDEtNCAzLTRzMyAxIDMgMmwtMyAyMCAxMi0xNGMxLTEgMy0yIDQtMSAyIDIgMiA0IDEgNkw1MSAzN2gxbDEyLTJjMy0xIDYtMiA3IDAgMSAxIDAgMyAwIDNsLTIxIDVjMTQgMSAxNSAwIDE4IDEgMiAwIDMgMiAzIDMgMCAzLTIgMy0zIDNsLTE5LTQgMTUgMTR2MWwtMiAxYy0xIDAtOS03LTE0LTExbDcgMTFjMSAxIDEgMyAwIDRzLTMgMS0zIDBMNDEgNTBjMCA3LTEgMTMtMiAxOSAwIDEtMSAxLTMgMi0xIDAtMy0xLTItM2wxLTQgMy0xNi0xMCAxMy00IDVoLTFjLTEgMC0yLTEtMi0zbDE0LTE4LTE3IDExaC00cy0xLTIgMC0zbDUtNHoiIGZpbGw9IiNENzUiLz48L3N2Zz4=
// @license      AGPL-v3.0
// @namespace    http://greasyfork.icu/zh-CN/scripts/502829-claude-helper
// @supportURL   http://greasyfork.icu/zh-CN/scripts/502829-claude-helper
// @homepageURL  http://greasyfork.icu/zh-CN/scripts/502829-claude-helper
// @grant        GM_addStyle
// ==/UserScript==

(function() {

  // model info
  function conversation_model() {
    let conversation = document.querySelector("body > div.flex.min-h-screen.w-full > div > div > div.relative.flex.w-full > div.relative.mx-auto");
    if(!conversation) return null;

    let reactProps = Object.keys(conversation).find(key => key.startsWith('__reactProps$'));
    if (!reactProps) return null;

    let conversProps = conversation[reactProps];
    if (!conversProps) return null;
    let model = conversProps.children[2]?.props?.conversation?.model; //claude-3-5-sonnet-20240620

    return model;
  }

  // msg count
  var last_uuid = '', last_length = 0;
  function get_msg_count() {
    let mainScreen = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ;
    if(!mainScreen) return;

    let tx_cnts = 0, tx_sz = 0;
    let rx_cnts = 0, rx_sz = 0;
    let fp_cnts = 0, fp_sz = 0, img_cnts = 0;
    let i = 0;

    let reactProps = Object.keys(mainScreen).find(key => key.startsWith('__reactProps$'));
    if (!reactProps) return null;

    let msgProps = mainScreen[reactProps];
    let Msgs = (msgProps.children[0]?.props?.messages);

    if (Msgs && Msgs.length > 0) {
      let newest_msgs = Msgs[Msgs.length-1];
      let uuid = newest_msgs.uuid;
      let length = newest_msgs.text.length;
      if (uuid == last_uuid && length == last_length) {
        return null;
      }
      last_uuid = uuid;
      last_length = length;
    } else {
      return null;
    }

    Msgs.forEach(function(msg){
      if(msg.sender == "human") {
        tx_cnts +=1;
        tx_sz += msg.text.length;
        for(i = 0; i < msg.attachments.length; i++) {
          tx_sz += msg.attachments[i].file_size;
          fp_cnts += 1;
          fp_sz += msg.attachments[i].file_size;;
        }
        img_cnts += msg.files.length;

      } else if(msg.sender == "assistant") {
        rx_cnts +=1;
        rx_sz += msg.text.length;
      }
    });

    return {
      tx_cnts: tx_cnts, tx_sz: tx_sz,
      rx_cnts: rx_cnts, rx_sz: rx_sz,
      fp_cnts: fp_cnts, fp_sz: fp_sz,
      img_cnts: img_cnts,
    };
  }

  function msg_counter_main() {
    let fieldset = document.querySelector("body > div.flex.min-h-screen.w-full fieldset");
    if (fieldset) {
      let ret = get_msg_count();
      let model = conversation_model();

      if(!ret) return;

      let count_result = document.querySelector("#claude-msg-counter")
      if(!count_result) {
        count_result = document.createElement("pre");
        count_result.id = "claude-msg-counter";
        count_result.className="border-0.5 relative z-[5] text-text-200 border-accent-pro-100/20 bg-accent-pro-900 rounded-t-xl border-b-0"
        count_result.style = "font-size:12px; padding: 5px 7px 14px; margin:-12px 0; text-wrap: pretty;";

        if (fieldset.querySelector("div.flex.md\\:px-2.flex-col-reverse > div") ){
          fieldset.querySelector("div.flex.md\\:px-2.flex-col-reverse > div").remove();
        }
        fieldset.querySelector("div.flex.md\\:px-2.flex-col-reverse").append(count_result);
      }

      let all_length = ret.tx_sz + ret.rx_sz ;
      let file_info = ""
      let img_file_info = ""
      if (ret.fp_cnts) file_info = ` (包含${ret.fp_cnts}个上传或粘贴文本,${ret.fp_sz}字)`
      if (ret.img_cnts) img_file_info = ` (另有${ret.img_cnts}个非文本内容的上传或粘贴,不能计量字数)`

      let model_info = '';
      if (model) {
        model_info = `【模型】${model}。`;
      }

      count_result.innerText = `【统计】已发出:${ret.tx_cnts}条,${ret.tx_sz}字${file_info}; 已回复:${ret.rx_cnts}条,${ret.rx_sz}字; 总计:${all_length}字${img_file_info}。${model_info}`;
    }
  }

  setInterval(() => {
    msg_counter_main();
  }, 1600);

  // show update time
  function show_msg_time() {
    let mainScreen = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ;
    if(!mainScreen) return;

    const msg_divs = mainScreen.querySelectorAll("div[data-test-render-count] > div.mb-1.mt-1, div[data-test-render-count] > div > div[data-is-streaming].group");

    msg_divs.forEach(function(msg_div){
      if (msg_div.nextSibling) return;
      let reactProps = Object.keys(msg_div).find(key => key.startsWith('__reactProps$'));
      if (!reactProps) return;
      let divProps = msg_div[reactProps];
      let updated_at = divProps.children?.[1]?.props?.message?.updated_at ?? divProps.children?.[1]?.props?.children?.[2]?.props?.message?.updated_at;
      //let created_at = divProps.children?.[1]?.props?.message?.created_at ?? divProps.children?.[1]?.props?.children?.[2]?.props?.message?.created_at;
      if (!updated_at) return;
      const date = new Date(updated_at);
      if (!date) return;
      const localDateStr = date.toLocaleString();
      let timeNode = document.createElement("div");
      timeNode.innerText = localDateStr;
      timeNode.className = 'msg-uptime';
      //console.log(updated_at, created_at);
      msg_div.after(timeNode);
    });
  }
  GM_addStyle(`
  div[data-test-render-count] > div > .msg-uptime {
     margin: 1px 5px 5px; font-size: 13px; font-weight: 300;
  }
  div[data-test-render-count] > .msg-uptime {
     margin: -2px 5px 5px; font-size: 13px; font-weight: 300;
  }
  `);
  setInterval(() => {
    show_msg_time();
  }, 2100);

  // Add Download Button
  function createPersistentElement(selector, createElementCallback) {
    function ensureElement() {
      const targetElement = document.querySelector(selector);
      if (targetElement) {
        if (!targetElement.querySelector('.-added-element')) {
          const newElement = createElementCallback();
          newElement.classList.add('-added-element');
          targetElement.appendChild(newElement);
        }
      }
    }

    ensureElement();
    const observer = new MutationObserver(() => {
      ensureElement();
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  function get_msg_context() {
    let context = "";
    let mainScreen = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ;
    if(!mainScreen) return;

    let tx_cnts = 0, tx_sz = 0;
    let rx_cnts = 0, rx_sz = 0;
    let fp_cnts = 0, fp_sz = 0;
    let i = 0;

    let reactProps = Object.keys(mainScreen).find(key => key.startsWith('__reactProps$'));
    if (!reactProps) return null;

    let msgProps = mainScreen[reactProps];

    let convID = (msgProps.children[0]?.props?.conversationUUID);
    let name = (msgProps.children[0]?.props?.name);
    let Msgs = (msgProps.children[0]?.props?.messages);

    if ( !convID || !name || !Msgs && !Msgs.length <= 0) {
      return null;
    }

    let model = conversation_model();
    let model_info = '';
    if (model) {
      model_info = `# Model: ${model}\n`;
    }

    context += `# ${name}\n${model_info}# conversationUUID: ${convID}\n`;

    Msgs.forEach(function(msg){
      context += `\n## ${msg.sender}:\n\n`
      context += msg.text + '\n'
      for(i = 0; i < msg.attachments.length; i++) {
        context += `file: ${msg.attachments[i].file_name}\n`
        if(msg.attachments[i].extracted_content) {
          context += `file_context: ${msg.attachments[i].extracted_content}\n`;
        }
      }
      for(i = 0; i < msg.files.length; i++) {
        context += `file: ${msg.files[i].file_name}\n`
        if(msg.files[i].preview_url) {
          context += `preview_url: ${window.location.origin + msg.files[i].preview_url}\n`;
        }
      }

      context += `\n------------------------------------------------------\n`
    });

    let blob = new Blob([context], {type: 'text/plain;charset=utf-8'});
    let fileUrl = URL.createObjectURL(blob);
    let tempLink = document.createElement('a');
    tempLink.href = fileUrl;

    let fileTitle = name.replaceAll(' ','_') + ".ClaudeAI.export.md";
    tempLink.setAttribute('download', fileTitle);
    tempLink.style.display = 'none';
    document.body.appendChild(tempLink);
    tempLink.click();
    document.body.removeChild(tempLink);
    URL.revokeObjectURL(fileUrl);

    return;
  }


  function createDownloadButton() {
    const button = document.createElement("button");
    button.className = "inline-flex items-center justify-center relative shrink-0 ring-offset-2 ring-offset-bg-300 ring-accent-main-100 focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none disabled:drop-shadow-none text-text-200 transition-all font-styrene active:bg-bg-400 hover:bg-bg-500/40 hover:text-text-100 h-9 w-9 rounded-md active:scale-95 shrink-0";
    button.innerHTML = `<svg width="20" height="20" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" fill="none"><path stroke="#535358" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M27 7H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h22a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2"/><path stroke="#535358" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 20v-8l-4 4-4-4v8m12-3.5 3.5 3.5 3.5-3.5M22.5 20v-9"/></svg>`;
    button.title="Download Conversation"
    button.addEventListener("click", () => {
      get_msg_context();
    });

    return button;
  }

  // 添加按钮
  createPersistentElement("body > div.flex.min-h-screen.w-full div.sticky.items-center div.right-3 div.hidden.flex-row-reverse", createDownloadButton);

})();