PromptEditor.svelte 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <script lang="ts">
  2. import { onMount, tick, getContext } from 'svelte';
  3. import Textarea from '$lib/components/common/Textarea.svelte';
  4. import { toast } from 'svelte-sonner';
  5. import Tooltip from '$lib/components/common/Tooltip.svelte';
  6. import AccessControl from '../common/AccessControl.svelte';
  7. import LockClosed from '$lib/components/icons/LockClosed.svelte';
  8. import AccessControlModal from '../common/AccessControlModal.svelte';
  9. export let onSubmit: Function;
  10. export let edit = false;
  11. export let prompt = null;
  12. const i18n = getContext('i18n');
  13. let loading = false;
  14. let title = '';
  15. let command = '';
  16. let content = '';
  17. let accessControl = null;
  18. let showAccessControlModal = false;
  19. $: if (!edit) {
  20. command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : '';
  21. }
  22. const submitHandler = async () => {
  23. loading = true;
  24. if (validateCommandString(command)) {
  25. await onSubmit({
  26. title,
  27. command,
  28. content,
  29. access_control: accessControl
  30. });
  31. } else {
  32. toast.error(
  33. $i18n.t('Only alphanumeric characters and hyphens are allowed in the command string.')
  34. );
  35. }
  36. loading = false;
  37. };
  38. const validateCommandString = (inputString) => {
  39. // Regular expression to match only alphanumeric characters and hyphen
  40. const regex = /^[a-zA-Z0-9-]+$/;
  41. // Test the input string against the regular expression
  42. return regex.test(inputString);
  43. };
  44. onMount(async () => {
  45. if (prompt) {
  46. title = prompt.title;
  47. await tick();
  48. command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
  49. content = prompt.content;
  50. accessControl = prompt?.access_control ?? null;
  51. }
  52. });
  53. </script>
  54. <AccessControlModal bind:show={showAccessControlModal} bind:accessControl />
  55. <div class="w-full max-h-full flex justify-center">
  56. <form
  57. class="flex flex-col w-full mb-10"
  58. on:submit|preventDefault={() => {
  59. submitHandler();
  60. }}
  61. >
  62. <div class="my-2">
  63. <Tooltip
  64. content={`${$i18n.t('Only alphanumeric characters and hyphens are allowed')} - ${$i18n.t(
  65. 'Activate this command by typing "/{{COMMAND}}" to chat input.',
  66. {
  67. COMMAND: command
  68. }
  69. )}`}
  70. placement="bottom-start"
  71. >
  72. <div class="flex flex-col w-full">
  73. <div class="flex items-center">
  74. <input
  75. class="text-2xl font-semibold w-full bg-transparent outline-none"
  76. placeholder={$i18n.t('Title')}
  77. bind:value={title}
  78. required
  79. />
  80. <div class="self-center flex-shrink-0">
  81. <button
  82. class="bg-gray-50 hover:bg-gray-100 text-black dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white transition px-2 py-1 rounded-full flex gap-1 items-center"
  83. type="button"
  84. on:click={() => {
  85. showAccessControlModal = true;
  86. }}
  87. >
  88. <LockClosed strokeWidth="2.5" className="size-3.5" />
  89. <div class="text-sm font-medium flex-shrink-0">
  90. {$i18n.t('Access')}
  91. </div>
  92. </button>
  93. </div>
  94. </div>
  95. <div class="flex gap-0.5 items-center text-xs text-gray-500">
  96. <div class="">/</div>
  97. <input
  98. class=" w-full bg-transparent outline-none"
  99. placeholder={$i18n.t('Command')}
  100. bind:value={command}
  101. required
  102. disabled={edit}
  103. />
  104. </div>
  105. </div>
  106. </Tooltip>
  107. </div>
  108. <div class="my-2">
  109. <div class="flex w-full justify-between">
  110. <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt Content')}</div>
  111. </div>
  112. <div class="mt-2">
  113. <div>
  114. <Textarea
  115. className="text-sm w-full bg-transparent outline-none overflow-y-hidden resize-none"
  116. placeholder={$i18n.t('Write a summary in 50 words that summarizes [topic or keyword].')}
  117. bind:value={content}
  118. rows={6}
  119. required
  120. />
  121. </div>
  122. <div class="text-xs text-gray-400 dark:text-gray-500">
  123. ⓘ {$i18n.t('Format your variables using brackets like this:')}&nbsp;<span
  124. class=" text-gray-600 dark:text-gray-300 font-medium"
  125. >{'{{'}{$i18n.t('variable')}{'}}'}</span
  126. >.
  127. {$i18n.t('Make sure to enclose them with')}
  128. <span class=" text-gray-600 dark:text-gray-300 font-medium">{'{{'}</span>
  129. {$i18n.t('and')}
  130. <span class=" text-gray-600 dark:text-gray-300 font-medium">{'}}'}</span>.
  131. </div>
  132. <div class="text-xs text-gray-400 dark:text-gray-500">
  133. {$i18n.t('Utilize')}<span class=" text-gray-600 dark:text-gray-300 font-medium">
  134. {` {{CLIPBOARD}}`}</span
  135. >
  136. {$i18n.t('variable to have them replaced with clipboard content.')}
  137. </div>
  138. </div>
  139. </div>
  140. <div class="my-4 flex justify-end pb-20">
  141. <button
  142. class=" text-sm w-full lg:w-fit px-4 py-2 transition rounded-lg {loading
  143. ? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
  144. : 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
  145. type="submit"
  146. disabled={loading}
  147. >
  148. <div class=" self-center font-medium">{$i18n.t('Save & Create')}</div>
  149. {#if loading}
  150. <div class="ml-1.5 self-center">
  151. <svg
  152. class=" w-4 h-4"
  153. viewBox="0 0 24 24"
  154. fill="currentColor"
  155. xmlns="http://www.w3.org/2000/svg"
  156. ><style>
  157. .spinner_ajPY {
  158. transform-origin: center;
  159. animation: spinner_AtaB 0.75s infinite linear;
  160. }
  161. @keyframes spinner_AtaB {
  162. 100% {
  163. transform: rotate(360deg);
  164. }
  165. }
  166. </style><path
  167. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  168. opacity=".25"
  169. /><path
  170. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  171. class="spinner_ajPY"
  172. /></svg
  173. >
  174. </div>
  175. {/if}
  176. </button>
  177. </div>
  178. </form>
  179. </div>