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

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

Stan na 05-09-2024. Zobacz najnowsza wersja.

// ==UserScript==
// @name        Claude helper (对话导出\字数统计\时间显示)
// @name:zh-CN  Claude 助手 (对话导出\字数统计\时间显示)
// @version      0.5.8
// @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    https://greasyfork.org/zh-CN/scripts/502829-claude-helper
// @supportURL   https://greasyfork.org/zh-CN/scripts/502829-claude-helper
// @homepageURL  https://greasyfork.org/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.flex.h-screen") ;
    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[1]?.props?.children[0]?.props?.conversation?.model; //claude-3-5-sonnet-20240620

    return model;
  }

  // tokensSoFar
  function conversation_tokensSoFar() {
    let conversation = document.querySelector("body > div.flex.min-h-screen.w-full > div > div.flex.h-screen") ;
    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 tokensSoFar = conversProps.children[1]?.props?.children[0]?.props?.conversation?.tokensSoFar;

    return tokensSoFar;
  }

  // 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();
      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}个非文本内容的上传或粘贴,不能计量字数)`

      const model = conversation_model();
      const token = conversation_tokensSoFar();

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

      let token_info = '';
      if (token) {
        token_info = `【tokensSoFar】${token}。`;
      }

      conversation_tokensSoFar

      count_result.innerText = `【统计】已发出:${ret.tx_cnts}条,${ret.tx_sz}字${file_info}; 已回复:${ret.rx_cnts}条,${ret.rx_sz}字; 总计:${all_length}字${img_file_info}。${model_info}${token_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;
    }

    const model = conversation_model();
    const token = conversation_tokensSoFar();

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

    let token_info = '';
    if (token) {
      token_info = `tokensSoFar: ${token}\n`;
    }

    context += `# ${name}\n${model_info}${token_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);

})();