Prompts.svelte 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <script lang="ts">
  2. import { prompts } from '$lib/stores';
  3. import { findWordIndices } from '$lib/utils';
  4. import { tick, getContext } from 'svelte';
  5. import { toast } from 'svelte-sonner';
  6. const i18n = getContext('i18n');
  7. export let files;
  8. export let prompt = '';
  9. export let command = '';
  10. let selectedPromptIdx = 0;
  11. let filteredPrompts = [];
  12. $: filteredPrompts = $prompts
  13. .filter((p) => p.command.toLowerCase().includes(command.toLowerCase()))
  14. .sort((a, b) => a.title.localeCompare(b.title));
  15. $: if (command) {
  16. selectedPromptIdx = 0;
  17. }
  18. export const selectUp = () => {
  19. selectedPromptIdx = Math.max(0, selectedPromptIdx - 1);
  20. };
  21. export const selectDown = () => {
  22. selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
  23. };
  24. const confirmPrompt = async (command) => {
  25. let text = command.content;
  26. if (command.content.includes('{{CLIPBOARD}}')) {
  27. const clipboardText = await navigator.clipboard.readText().catch((err) => {
  28. toast.error($i18n.t('Failed to read clipboard contents'));
  29. return '{{CLIPBOARD}}';
  30. });
  31. console.log(clipboardText);
  32. const clipboardItems = await navigator.clipboard.read();
  33. let imageUrl = null;
  34. for (const item of clipboardItems) {
  35. // Check for known image types
  36. for (const type of item.types) {
  37. if (type.startsWith('image/')) {
  38. const blob = await item.getType(type);
  39. imageUrl = URL.createObjectURL(blob);
  40. console.log(`Image URL (${type}): ${imageUrl}`);
  41. }
  42. }
  43. }
  44. if (imageUrl) {
  45. files = [
  46. ...files,
  47. {
  48. type: 'image',
  49. url: imageUrl
  50. }
  51. ];
  52. }
  53. text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
  54. }
  55. prompt = text;
  56. const chatInputElement = document.getElementById('chat-textarea');
  57. await tick();
  58. chatInputElement.style.height = '';
  59. chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
  60. chatInputElement?.focus();
  61. await tick();
  62. const words = findWordIndices(prompt);
  63. if (words.length > 0) {
  64. const word = words.at(0);
  65. chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
  66. }
  67. };
  68. </script>
  69. {#if filteredPrompts.length > 0}
  70. <div
  71. id="commands-container"
  72. class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
  73. >
  74. <div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
  75. <div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
  76. <div class=" text-lg font-semibold mt-2">/</div>
  77. </div>
  78. <div
  79. class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
  80. >
  81. <div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
  82. {#each filteredPrompts as prompt, promptIdx}
  83. <button
  84. class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
  85. ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
  86. : ''}"
  87. type="button"
  88. on:click={() => {
  89. confirmPrompt(prompt);
  90. }}
  91. on:mousemove={() => {
  92. selectedPromptIdx = promptIdx;
  93. }}
  94. on:focus={() => {}}
  95. >
  96. <div class=" font-medium text-black dark:text-gray-100">
  97. {prompt.command}
  98. </div>
  99. <div class=" text-xs text-gray-600 dark:text-gray-100">
  100. {prompt.title}
  101. </div>
  102. </button>
  103. {/each}
  104. </div>
  105. <div
  106. class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-br-xl flex items-center space-x-1"
  107. >
  108. <div>
  109. <svg
  110. xmlns="http://www.w3.org/2000/svg"
  111. fill="none"
  112. viewBox="0 0 24 24"
  113. stroke-width="1.5"
  114. stroke="currentColor"
  115. class="w-3 h-3"
  116. >
  117. <path
  118. stroke-linecap="round"
  119. stroke-linejoin="round"
  120. 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"
  121. />
  122. </svg>
  123. </div>
  124. <div class="line-clamp-1">
  125. {$i18n.t(
  126. 'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
  127. )}
  128. </div>
  129. </div>
  130. </div>
  131. </div>
  132. </div>
  133. {/if}