ContentRenderer.svelte 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. <script>
  2. import { onDestroy, onMount, tick, getContext, createEventDispatcher } from 'svelte';
  3. const i18n = getContext('i18n');
  4. const dispatch = createEventDispatcher();
  5. import Markdown from './Markdown.svelte';
  6. import LightBlub from '$lib/components/icons/LightBlub.svelte';
  7. import { chatId, mobile, showArtifacts, showControls, showOverview } from '$lib/stores';
  8. import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
  9. export let id;
  10. export let content;
  11. export let model = null;
  12. export let save = false;
  13. export let floatingButtons = true;
  14. let contentContainerElement;
  15. let buttonsContainerElement;
  16. let selectedText = '';
  17. let floatingInput = false;
  18. let floatingInputValue = '';
  19. const updateButtonPosition = (event) => {
  20. setTimeout(async () => {
  21. await tick();
  22. // Check if the event target is within the content container
  23. if (!contentContainerElement?.contains(event.target)) {
  24. closeFloatingButtons();
  25. return;
  26. }
  27. let selection = window.getSelection();
  28. if (selection.toString().trim().length > 0) {
  29. floatingInput = false;
  30. const range = selection.getRangeAt(0);
  31. const rect = range.getBoundingClientRect();
  32. const parentRect = contentContainerElement.getBoundingClientRect();
  33. // Adjust based on parent rect
  34. const top = rect.bottom - parentRect.top;
  35. const left = rect.left - parentRect.left;
  36. if (buttonsContainerElement) {
  37. buttonsContainerElement.style.display = 'block';
  38. // Calculate space available on the right
  39. const spaceOnRight = parentRect.width - (left + buttonsContainerElement.offsetWidth);
  40. if (spaceOnRight < 0) {
  41. // Not enough space on the right, position using 'right'
  42. const right = parentRect.right - rect.right;
  43. buttonsContainerElement.style.right = `${right}px`;
  44. buttonsContainerElement.style.left = 'auto'; // Reset left
  45. } else {
  46. // Enough space, position using 'left'
  47. buttonsContainerElement.style.left = `${left}px`;
  48. buttonsContainerElement.style.right = 'auto'; // Reset right
  49. }
  50. buttonsContainerElement.style.top = `${top + 5}px`; // +5 to add some spacing
  51. }
  52. } else {
  53. closeFloatingButtons();
  54. }
  55. }, 0);
  56. };
  57. const closeFloatingButtons = () => {
  58. if (buttonsContainerElement) {
  59. buttonsContainerElement.style.display = 'none';
  60. selectedText = '';
  61. floatingInput = false;
  62. floatingInputValue = '';
  63. }
  64. };
  65. const selectAskHandler = () => {
  66. dispatch('select', {
  67. type: 'ask',
  68. content: selectedText,
  69. input: floatingInputValue
  70. });
  71. floatingInput = false;
  72. floatingInputValue = '';
  73. selectedText = '';
  74. // Clear selection
  75. window.getSelection().removeAllRanges();
  76. buttonsContainerElement.style.display = 'none';
  77. };
  78. const keydownHandler = (e) => {
  79. if (e.key === 'Escape') {
  80. closeFloatingButtons();
  81. }
  82. };
  83. onMount(() => {
  84. if (floatingButtons) {
  85. contentContainerElement?.addEventListener('mouseup', updateButtonPosition);
  86. document.addEventListener('mouseup', updateButtonPosition);
  87. document.addEventListener('keydown', keydownHandler);
  88. }
  89. });
  90. onDestroy(() => {
  91. if (floatingButtons) {
  92. contentContainerElement?.removeEventListener('mouseup', updateButtonPosition);
  93. document.removeEventListener('mouseup', updateButtonPosition);
  94. document.removeEventListener('keydown', keydownHandler);
  95. }
  96. });
  97. </script>
  98. <div bind:this={contentContainerElement}>
  99. <Markdown
  100. {id}
  101. {content}
  102. {model}
  103. {save}
  104. on:update={(e) => {
  105. dispatch('update', e.detail);
  106. }}
  107. on:code={(e) => {
  108. const { lang, code } = e.detail;
  109. if (
  110. (['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) &&
  111. !$mobile &&
  112. $chatId
  113. ) {
  114. showArtifacts.set(true);
  115. showControls.set(true);
  116. }
  117. }}
  118. />
  119. </div>
  120. {#if floatingButtons}
  121. <div
  122. bind:this={buttonsContainerElement}
  123. class="absolute rounded-lg mt-1 text-xs z-[9999]"
  124. style="display: none"
  125. >
  126. {#if !floatingInput}
  127. <div
  128. class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl"
  129. >
  130. <button
  131. class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
  132. on:click={() => {
  133. selectedText = window.getSelection().toString();
  134. floatingInput = true;
  135. }}
  136. >
  137. <ChatBubble className="size-3 shrink-0" />
  138. <div class="shrink-0">Ask</div>
  139. </button>
  140. <button
  141. class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
  142. on:click={() => {
  143. const selection = window.getSelection();
  144. dispatch('select', {
  145. type: 'explain',
  146. content: selection.toString()
  147. });
  148. // Clear selection
  149. selection.removeAllRanges();
  150. buttonsContainerElement.style.display = 'none';
  151. }}
  152. >
  153. <LightBlub className="size-3 shrink-0" />
  154. <div class="shrink-0">Explain</div>
  155. </button>
  156. </div>
  157. {:else}
  158. <div
  159. class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border dark:border-gray-800 w-72 rounded-full shadow-xl"
  160. >
  161. <input
  162. type="text"
  163. class="ml-5 bg-transparent outline-none w-full flex-1 text-sm"
  164. placeholder={$i18n.t('Ask a question')}
  165. bind:value={floatingInputValue}
  166. on:keydown={(e) => {
  167. if (e.key === 'Enter') {
  168. selectAskHandler();
  169. }
  170. }}
  171. />
  172. <div class="ml-1 mr-2">
  173. <button
  174. class="{floatingInputValue !== ''
  175. ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
  176. : 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 m-0.5 self-center"
  177. on:click={() => {
  178. selectAskHandler();
  179. }}
  180. >
  181. <svg
  182. xmlns="http://www.w3.org/2000/svg"
  183. viewBox="0 0 16 16"
  184. fill="currentColor"
  185. class="size-4"
  186. >
  187. <path
  188. fill-rule="evenodd"
  189. d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
  190. clip-rule="evenodd"
  191. />
  192. </svg>
  193. </button>
  194. </div>
  195. </div>
  196. {/if}
  197. </div>
  198. {/if}