Kagi Assistant Enhancements

Adds prompt library and code copy features to Kagi Assistant

  1. // ==UserScript==
  2. // @name Kagi Assistant Enhancements
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description Adds prompt library and code copy features to Kagi Assistant
  6. // @author You
  7. // @match https://kagi.com/assistant/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // ===== Prompt Library Functions =====
  16.  
  17. // Load prompts from localStorage
  18. function loadPrompts() {
  19. const savedPrompts = localStorage.getItem('kagiPrompts');
  20. return savedPrompts ? JSON.parse(savedPrompts) : [
  21. { name: "Example Prompt 1", text: "This is example prompt 1" },
  22. { name: "Example Prompt 2", text: "This is example prompt 2" }
  23. ];
  24. }
  25.  
  26. // Save prompts to localStorage
  27. function savePrompts(prompts) {
  28. localStorage.setItem('kagiPrompts', JSON.stringify(prompts));
  29. }
  30.  
  31. // ===== Code Copy Button Functions =====
  32.  
  33. // Add copy button to code blocks
  34. function addCopyButton() {
  35. const codeBlocks = document.querySelectorAll('.codehilite');
  36.  
  37. codeBlocks.forEach(block => {
  38. if (block.querySelector('.bottom-copy-btn')) return;
  39.  
  40. const copyButton = document.createElement('button');
  41. copyButton.className = 'bottom-copy-btn relative';
  42. copyButton.title = 'Copy';
  43. copyButton.setAttribute('data-partial-update-ignore', 'true');
  44. copyButton.innerHTML = `
  45. <span class="_0_copied_tooltip">Copied to clipboard</span>
  46. <i class="icon-sm">
  47. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  48. <path d="M7.5 7.5V5.25A2.25 2.25 0 019.75 3H18.75A2.25 2.25 0 0121 5.25V14.25A2.25 2.25 0 0118.75 16.5H16.5M16.5 9.75A2.25 2.25 0 0014.25 7.5H5.25A2.25 2.25 0 003 9.75V18.75A2.25 2.25 0 005.25 21H14.25A2.25 2.25 0 0016.5 18.75V9.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
  49. </svg>
  50. </i>
  51. `;
  52.  
  53. copyButton.addEventListener('click', async () => {
  54. const code = block.querySelector('code').textContent;
  55. await navigator.clipboard.writeText(code);
  56.  
  57. const tooltip = copyButton.querySelector('._0_copied_tooltip');
  58. tooltip.style.display = 'block';
  59. setTimeout(() => {
  60. tooltip.style.display = 'none';
  61. }, 2000);
  62. });
  63.  
  64. block.appendChild(copyButton);
  65. });
  66. }
  67.  
  68. // ===== Create Prompt Library UI =====
  69. function createPromptLibrary() {
  70. const button = document.createElement('button');
  71. button.type = 'button';
  72. button.id = 'prompt-library-button';
  73. button.className = 'prompt-library';
  74. button.innerHTML = `
  75. <i class="icon-md">
  76. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  77. <path d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z" stroke="currentColor" stroke-width="1.5"/>
  78. <path d="M7 7H17M7 12H17M7 17H13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
  79. </svg>
  80. </i>
  81. <span>Prompts</span>
  82. `;
  83.  
  84. const wrapper = document.createElement('div');
  85. wrapper.style.position = 'relative';
  86. wrapper.appendChild(button);
  87.  
  88. // Create dropdown menu
  89. const dropdown = document.createElement('div');
  90. dropdown.className = 'prompt-dropdown';
  91. dropdown.style.display = 'none';
  92.  
  93. // Update dropdown content function
  94. function updateDropdown() {
  95. const prompts = loadPrompts();
  96. dropdown.innerHTML = `
  97. <div class="prompt-header">
  98. <span>My Prompts</span>
  99. <div class="prompt-header-actions">
  100. <button class="export-btn" title="Export">📤</button>
  101. <button class="import-btn" title="Import">📥</button>
  102. <button class="add-prompt-btn">+</button>
  103. </div>
  104. </div>
  105. <div class="prompt-list">
  106. ${prompts.map((prompt, index) => `
  107. <div class="prompt-item">
  108. <span class="prompt-name">${prompt.name}</span>
  109. <div class="prompt-actions">
  110. <button class="edit-btn" data-index="${index}">✏️</button>
  111. <button class="delete-btn" data-index="${index}">🗑️</button>
  112. </div>
  113. </div>
  114. `).join('')}
  115. </div>
  116. `;
  117.  
  118. // Add event listeners for dropdown buttons
  119. attachDropdownListeners(dropdown);
  120. }
  121.  
  122. // Handle dropdown visibility
  123. button.onclick = (e) => {
  124. e.stopPropagation();
  125. if (dropdown.style.display === 'none') {
  126. updateDropdown();
  127. dropdown.style.display = 'block';
  128. } else {
  129. dropdown.style.display = 'none';
  130. }
  131. };
  132.  
  133. document.addEventListener('click', (e) => {
  134. if (!wrapper.contains(e.target)) {
  135. dropdown.style.display = 'none';
  136. }
  137. });
  138.  
  139. wrapper.appendChild(dropdown);
  140. return wrapper;
  141. }
  142.  
  143. // Attach event listeners to dropdown elements
  144. function attachDropdownListeners(dropdown) {
  145. // Export functionality
  146. dropdown.querySelector('.export-btn').onclick = (e) => {
  147. e.stopPropagation();
  148. const prompts = loadPrompts();
  149. const blob = new Blob([JSON.stringify(prompts, null, 2)], {type: 'application/json'});
  150. const url = URL.createObjectURL(blob);
  151. const a = document.createElement('a');
  152. a.href = url;
  153. a.download = 'kagi-prompts.json';
  154. document.body.appendChild(a);
  155. a.click();
  156. document.body.removeChild(a);
  157. URL.revokeObjectURL(url);
  158. };
  159.  
  160. // Import functionality
  161. dropdown.querySelector('.import-btn').onclick = (e) => {
  162. e.stopPropagation();
  163. const input = document.createElement('input');
  164. input.type = 'file';
  165. input.accept = '.json';
  166. input.onchange = async (e) => {
  167. try {
  168. const file = e.target.files[0];
  169. const text = await file.text();
  170. const prompts = JSON.parse(text);
  171. if (Array.isArray(prompts) && prompts.every(p => p.name && p.text)) {
  172. if (confirm('Replace all current prompts?')) {
  173. savePrompts(prompts);
  174. dropdown.querySelector('.prompt-list').innerHTML = '';
  175. updateDropdown();
  176. }
  177. } else {
  178. alert('Invalid file format');
  179. }
  180. } catch (error) {
  181. alert('Import error');
  182. console.error(error);
  183. }
  184. };
  185. input.click();
  186. };
  187.  
  188. // Add new prompt
  189. dropdown.querySelector('.add-prompt-btn').onclick = (e) => {
  190. e.stopPropagation();
  191. const name = prompt("Prompt name:");
  192. const text = prompt("Prompt text:");
  193. if (name && text) {
  194. const prompts = loadPrompts();
  195. prompts.push({ name, text });
  196. savePrompts(prompts);
  197. updateDropdown();
  198. }
  199. };
  200.  
  201. // Handle prompt items
  202. dropdown.querySelectorAll('.prompt-item').forEach(item => {
  203. const promptBox = document.getElementById('promptBox');
  204.  
  205. // Click on prompt name to use it
  206. item.querySelector('.prompt-name').onclick = () => {
  207. const index = item.querySelector('.edit-btn').dataset.index;
  208. const prompts = loadPrompts();
  209. if (promptBox) {
  210. promptBox.value = prompts[index].text;
  211. promptBox.focus();
  212. }
  213. dropdown.style.display = 'none';
  214. };
  215.  
  216. // Edit prompt
  217. item.querySelector('.edit-btn').onclick = (e) => {
  218. e.stopPropagation();
  219. const index = e.target.dataset.index;
  220. const prompts = loadPrompts();
  221. const name = prompt("New name:", prompts[index].name);
  222. const text = prompt("New text:", prompts[index].text);
  223. if (name && text) {
  224. prompts[index] = { name, text };
  225. savePrompts(prompts);
  226. updateDropdown();
  227. }
  228. };
  229.  
  230. // Delete prompt
  231. item.querySelector('.delete-btn').onclick = (e) => {
  232. e.stopPropagation();
  233. const index = e.target.dataset.index;
  234. if (confirm("Delete this prompt?")) {
  235. const prompts = loadPrompts();
  236. prompts.splice(index, 1);
  237. savePrompts(prompts);
  238. updateDropdown();
  239. }
  240. };
  241. });
  242. }
  243.  
  244. // ===== Add Styles =====
  245. function addStyles() {
  246. const styles = document.createElement('style');
  247. styles.textContent = `
  248. .codehilite {
  249. position: relative;
  250. }
  251. .bottom-copy-btn {
  252. position: absolute;
  253. bottom: 10px;
  254. right: 10px;
  255. background: transparent;
  256. border: none;
  257. cursor: pointer;
  258. padding: 5px;
  259. opacity: 0.6;
  260. transition: opacity 0.2s;
  261. }
  262. .bottom-copy-btn:hover {
  263. opacity: 1;
  264. }
  265. .bottom-copy-btn ._0_copied_tooltip {
  266. display: none;
  267. position: absolute;
  268. bottom: 100%;
  269. right: 0;
  270. background: black;
  271. color: white;
  272. padding: 5px 10px;
  273. border-radius: 4px;
  274. font-size: 12px;
  275. white-space: nowrap;
  276. }
  277. .prompt-library {
  278. display: flex;
  279. align-items: center;
  280. gap: 8px;
  281. background: none;
  282. border: none;
  283. cursor: pointer;
  284. padding: 5px 10px;
  285. color: var(--color-text-primary);
  286. }
  287. .prompt-dropdown {
  288. position: absolute;
  289. background-color: rgb(255, 255, 255);
  290. border: 1px solid rgba(0, 0, 0, 0.1);
  291. border-radius: 8px;
  292. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  293. z-index: 1000;
  294. bottom: 100%;
  295. left: 50%;
  296. transform: translateX(-50%);
  297. min-width: 250px;
  298. max-height: 400px;
  299. overflow-y: auto;
  300. margin-bottom: 10px;
  301. }
  302. .prompt-header {
  303. display: flex;
  304. justify-content: space-between;
  305. align-items: center;
  306. padding: 8px 12px;
  307. border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  308. background-color: rgb(250, 250, 250);
  309. }
  310. .prompt-header-actions {
  311. display: flex;
  312. gap: 8px;
  313. align-items: center;
  314. }
  315. .add-prompt-btn, .export-btn, .import-btn {
  316. padding: 2px 8px;
  317. border-radius: 4px;
  318. border: 1px solid rgba(0, 0, 0, 0.1);
  319. background: white;
  320. cursor: pointer;
  321. }
  322. .prompt-item {
  323. display: flex;
  324. justify-content: space-between;
  325. align-items: center;
  326. padding: 8px 12px;
  327. cursor: pointer;
  328. background-color: rgb(255, 255, 255);
  329. }
  330. .prompt-item:hover {
  331. background-color: rgb(245, 245, 245);
  332. }
  333. .prompt-actions {
  334. display: flex;
  335. gap: 4px;
  336. }
  337. .prompt-actions button {
  338. padding: 2px 4px;
  339. border: none;
  340. background: none;
  341. cursor: pointer;
  342. opacity: 0.6;
  343. }
  344. .prompt-actions button:hover {
  345. opacity: 1;
  346. }
  347. .prompt-name {
  348. flex: 1;
  349. white-space: nowrap;
  350. overflow: hidden;
  351. text-overflow: ellipsis;
  352. margin-right: 8px;
  353. }
  354. `;
  355. document.head.appendChild(styles);
  356. }
  357.  
  358. // ===== Initialize =====
  359. function init() {
  360. // Add styles
  361. addStyles();
  362.  
  363. // Add prompt library
  364. const promptOptions = document.querySelector('.prompt-options');
  365. if (promptOptions && !document.getElementById('prompt-library-button')) {
  366. const promptLibrary = createPromptLibrary();
  367. const toggleSwitch = promptOptions.querySelector('.k_ui_toggle_switch');
  368. promptOptions.insertBefore(promptLibrary, toggleSwitch);
  369. }
  370.  
  371. // Add copy buttons to code blocks
  372. addCopyButton();
  373. }
  374.  
  375. // Observe DOM changes
  376. const observer = new MutationObserver((mutations) => {
  377. init();
  378. });
  379.  
  380. observer.observe(document.body, {
  381. childList: true,
  382. subtree: true
  383. });
  384.  
  385. // Initial execution
  386. init();
  387. })();