Prompts.svelte 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. <script lang="ts">
  2. import { prompts, settings, user } from '$lib/stores';
  3. import {
  4. findWordIndices,
  5. getUserPosition,
  6. getFormattedDate,
  7. getFormattedTime,
  8. getCurrentDateTime,
  9. getUserTimezone,
  10. getWeekday
  11. } from '$lib/utils';
  12. import { tick, getContext } from 'svelte';
  13. import { toast } from 'svelte-sonner';
  14. const i18n = getContext('i18n');
  15. export let files;
  16. export let prompt = '';
  17. export let command = '';
  18. let selectedPromptIdx = 0;
  19. let filteredPrompts = [];
  20. $: filteredPrompts = $prompts
  21. .filter((p) => p.command.toLowerCase().includes(command.toLowerCase()))
  22. .sort((a, b) => a.title.localeCompare(b.title));
  23. $: if (command) {
  24. selectedPromptIdx = 0;
  25. }
  26. export const selectUp = () => {
  27. selectedPromptIdx = Math.max(0, selectedPromptIdx - 1);
  28. };
  29. export const selectDown = () => {
  30. selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
  31. };
  32. const confirmPrompt = async (command) => {
  33. let text = command.content;
  34. if (command.content.includes('{{CLIPBOARD}}')) {
  35. const clipboardText = await navigator.clipboard.readText().catch((err) => {
  36. toast.error($i18n.t('Failed to read clipboard contents'));
  37. return '{{CLIPBOARD}}';
  38. });
  39. const clipboardItems = await navigator.clipboard.read();
  40. let imageUrl = null;
  41. for (const item of clipboardItems) {
  42. // Check for known image types
  43. for (const type of item.types) {
  44. if (type.startsWith('image/')) {
  45. const blob = await item.getType(type);
  46. imageUrl = URL.createObjectURL(blob);
  47. }
  48. }
  49. }
  50. if (imageUrl) {
  51. files = [
  52. ...files,
  53. {
  54. type: 'image',
  55. url: imageUrl
  56. }
  57. ];
  58. }
  59. text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
  60. }
  61. if (command.content.includes('{{USER_LOCATION}}')) {
  62. let location;
  63. try {
  64. location = await getUserPosition();
  65. } catch (error) {
  66. toast.error($i18n.t('Location access not allowed'));
  67. location = 'LOCATION_UNKNOWN';
  68. }
  69. text = text.replaceAll('{{USER_LOCATION}}', String(location));
  70. }
  71. if (command.content.includes('{{USER_NAME}}')) {
  72. console.log($user);
  73. const name = $user.name || 'User';
  74. text = text.replaceAll('{{USER_NAME}}', name);
  75. }
  76. if (command.content.includes('{{USER_LANGUAGE}}')) {
  77. const language = localStorage.getItem('locale') || 'en-US';
  78. text = text.replaceAll('{{USER_LANGUAGE}}', language);
  79. }
  80. if (command.content.includes('{{CURRENT_DATE}}')) {
  81. const date = getFormattedDate();
  82. text = text.replaceAll('{{CURRENT_DATE}}', date);
  83. }
  84. if (command.content.includes('{{CURRENT_TIME}}')) {
  85. const time = getFormattedTime();
  86. text = text.replaceAll('{{CURRENT_TIME}}', time);
  87. }
  88. if (command.content.includes('{{CURRENT_DATETIME}}')) {
  89. const dateTime = getCurrentDateTime();
  90. text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
  91. }
  92. if (command.content.includes('{{CURRENT_TIMEZONE}}')) {
  93. const timezone = getUserTimezone();
  94. text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
  95. }
  96. if (command.content.includes('{{CURRENT_WEEKDAY}}')) {
  97. const weekday = getWeekday();
  98. text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
  99. }
  100. const lines = prompt.split('\n');
  101. const lastLine = lines.pop();
  102. const lastLineWords = lastLine.split(' ');
  103. const lastWord = lastLineWords.pop();
  104. if ($settings?.richTextInput ?? true) {
  105. lastLineWords.push(`${text.replace(/</g, '&lt;').replace(/>/g, '&gt;')}`);
  106. lines.push(lastLineWords.join(' '));
  107. } else {
  108. lastLineWords.push(text);
  109. lines.push(lastLineWords.join(' '));
  110. }
  111. prompt = lines.join('\n');
  112. const chatInputContainerElement = document.getElementById('chat-input-container');
  113. const chatInputElement = document.getElementById('chat-input');
  114. await tick();
  115. if (chatInputContainerElement) {
  116. chatInputContainerElement.style.height = '';
  117. chatInputContainerElement.style.height =
  118. Math.min(chatInputContainerElement.scrollHeight, 200) + 'px';
  119. }
  120. await tick();
  121. if (chatInputElement) {
  122. chatInputElement.focus();
  123. chatInputElement.dispatchEvent(new Event('input'));
  124. }
  125. };
  126. </script>
  127. {#if filteredPrompts.length > 0}
  128. <div
  129. id="commands-container"
  130. class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
  131. >
  132. <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
  133. <div
  134. class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
  135. >
  136. <div class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden">
  137. {#each filteredPrompts as prompt, promptIdx}
  138. <button
  139. class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
  140. ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
  141. : ''}"
  142. type="button"
  143. on:click={() => {
  144. confirmPrompt(prompt);
  145. }}
  146. on:mousemove={() => {
  147. selectedPromptIdx = promptIdx;
  148. }}
  149. on:focus={() => {}}
  150. >
  151. <div class=" font-medium text-black dark:text-gray-100">
  152. {prompt.command}
  153. </div>
  154. <div class=" text-xs text-gray-600 dark:text-gray-100">
  155. {prompt.title}
  156. </div>
  157. </button>
  158. {/each}
  159. </div>
  160. <div
  161. class=" px-2 pt-0.5 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-b-xl flex items-center space-x-1"
  162. >
  163. <div>
  164. <svg
  165. xmlns="http://www.w3.org/2000/svg"
  166. fill="none"
  167. viewBox="0 0 24 24"
  168. stroke-width="1.5"
  169. stroke="currentColor"
  170. class="w-3 h-3"
  171. >
  172. <path
  173. stroke-linecap="round"
  174. stroke-linejoin="round"
  175. d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
  176. />
  177. </svg>
  178. </div>
  179. <div class="line-clamp-1">
  180. {$i18n.t(
  181. 'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
  182. )}
  183. </div>
  184. </div>
  185. </div>
  186. </div>
  187. </div>
  188. {/if}