Discourse AI Write

Advanced AI Write, powered by Gemini. Write your discourse posts with ease. Quickly ask questions.

  1. // ==UserScript==
  2. // @name Discourse AI Write
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description Advanced AI Write, powered by Gemini. Write your discourse posts with ease. Quickly ask questions.
  6. // @author ethandacat
  7. // @match https://x-camp.discourse.group/*
  8. // @icon https://avatars.githubusercontent.com/u/78333227?s=200&v=4
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. let now = new Date();
  14. let sessionId = `${now.getFullYear().toString() +
  15. (now.getMonth() + 1).toString().padStart(2, '0') +
  16. now.getDate().toString().padStart(2, '0') +
  17. now.getHours().toString().padStart(2, '0') +
  18. now.getMinutes().toString().padStart(2, '0') +
  19. now.getSeconds().toString().padStart(2, '0') +
  20. now.getMilliseconds().toString().padStart(3, '0')}${Math.floor(Math.random() * 1000) + 1}`;
  21.  
  22. console.log(sessionId);
  23.  
  24.  
  25. async function askGemini(prompt) {
  26. try {
  27. const res = await fetch('https://ai-write-nine.vercel.app/proxy/', {
  28. method: 'POST',
  29. headers: { 'Content-Type': 'application/json' },
  30. body: JSON.stringify({
  31. prompt,
  32. session_id: sessionId // or make this dynamic if you want per-editor-session chats
  33. })
  34. });
  35.  
  36. if (!res.ok) return `(Gemini error ${res.status})`;
  37. const data = await res.json();
  38. const text = data?.candidates?.[0]?.content?.parts?.[0]?.text;
  39. return text ?? "(no response)";
  40. } catch (err) {
  41. return `(Fetch error: ${err.message})`;
  42. }
  43. }
  44.  
  45. function addButton() {
  46. const bar = document.querySelector(".d-editor-button-bar");
  47. if (!bar || document.querySelector(".ai-write-button")) return;
  48.  
  49. const button = document.createElement("button");
  50. button.className = "btn no-text btn-icon quote ai-write-button";
  51. button.tabIndex = 0;
  52. button.title = "AI Write";
  53. button.innerHTML = `
  54. <svg class="fa d-icon d-icon-microchip-ai svg-icon svg-string" xmlns="http://www.w3.org/2000/svg">
  55. <use href="#robot"></use>
  56. </svg>
  57. `;
  58.  
  59. button.addEventListener("click", () => {
  60. const pophtml = `
  61. <div class="modal-container">
  62. <div class="modal d-modal poll-ui-builder" data-keyboard="false" aria-modal="true" role="dialog" aria-labelledby="discourse-modal-title">
  63. <div class="d-modal__container">
  64. <div class="d-modal__header">
  65. <div class="d-modal__title">
  66. <h1 id="discourse-modal-title" class="d-modal__title-text">Discourse AI Write</h1>
  67. </div>
  68. <button class="btn no-text btn-icon btn-transparent modal-close d-ai-close" title="close" type="button">
  69. <svg class="fa d-icon d-icon-xmark svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#xmark"></use></svg>
  70. <span aria-hidden="true">​</span>
  71. </button>
  72. </div>
  73. <div class="d-modal__body" tabindex="-1">
  74. <div class="poll-options">
  75. <p>What should Gemini revise or generate?</p>
  76. <div class="input-group poll-option-value">
  77. <input type="text" autofocus class="d-ai-inp">
  78. </div>
  79. <p>Thank you for using Discourse AI Write by Ethan!</p>
  80. </div>
  81. </div>
  82. <div class="d-modal__footer">
  83. <button class="btn btn-icon-text btn-primary d-ai-gen" type="button">
  84. <span class="d-button-label">Generate!</span>
  85. </button>
  86. <button class="btn btn-text btn-flat d-ai-close2" type="button">
  87. <span class="d-button-label">cancel</span>
  88. </button>
  89. </div>
  90. </div>
  91. </div>
  92. <div class="d-modal__backdrop"></div>
  93. </div>
  94. `;
  95.  
  96. const pop = document.createElement("div");
  97. pop.innerHTML = pophtml;
  98. document.querySelector(".discourse-root").appendChild(pop);
  99.  
  100. document.querySelector(".d-ai-close").onclick = () => document.querySelector(".modal-container")?.remove();
  101. document.querySelector(".d-ai-close2").onclick = () => document.querySelector(".modal-container")?.remove();
  102.  
  103. const input = document.querySelector(".d-ai-inp");
  104. const genBtn = document.querySelector(".d-ai-gen");
  105.  
  106. genBtn.onclick = async () => {
  107. const inptxt = input.value;
  108. const modal = document.querySelector(".modal-container");
  109.  
  110. genBtn.disabled = true;
  111. genBtn.innerHTML = "<span class='d-button-label'>Loading...</span>";
  112.  
  113. const textarea = document.querySelector("textarea.d-editor-input");
  114. if (textarea) {
  115. const result = await askGemini(inptxt);
  116. textarea.value = result;
  117. textarea.dispatchEvent(new Event("input", { bubbles: true }));
  118. }
  119. document.querySelector(".modal-container").remove();
  120. document.querySelector(".modal-container").remove();
  121. document.querySelector(".modal-container").remove();
  122. };
  123.  
  124. input.addEventListener("keydown", e => {
  125. if (e.key === "Enter") genBtn.click();
  126. else if (e.key === "Escape") document.querySelector(".modal-container")?.remove();
  127. });
  128. });
  129.  
  130. bar.appendChild(button);
  131. console.log("AI Write button added");
  132. }
  133.  
  134. function reliableWaitForEditor(callback) {
  135. let lastEditor = null;
  136. const check = () => {
  137. const editor = document.querySelector(".d-editor-button-bar");
  138. if (editor && editor !== lastEditor) {
  139. lastEditor = editor;
  140. callback();
  141. }
  142. };
  143. setInterval(check, 300);
  144. document.addEventListener("visibilitychange", check);
  145. window.addEventListener("hashchange", check);
  146. window.addEventListener("focus", check);
  147. }
  148.  
  149. reliableWaitForEditor(addButton);