ContentRenderer.svelte 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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)) return;
  24. let selection = window.getSelection();
  25. if (selection.toString().trim().length > 0) {
  26. floatingInput = false;
  27. const range = selection.getRangeAt(0);
  28. const rect = range.getBoundingClientRect();
  29. // Calculate position relative to the viewport (now that it's in document.body)
  30. const top = rect.bottom + window.scrollY;
  31. const left = rect.left + window.scrollX;
  32. if (buttonsContainerElement) {
  33. buttonsContainerElement.style.display = 'block';
  34. buttonsContainerElement.style.left = `${left}px`;
  35. buttonsContainerElement.style.top = `${top + 5}px`; // +5 to add some spacing
  36. }
  37. } else {
  38. if (buttonsContainerElement) {
  39. buttonsContainerElement.style.display = 'none';
  40. selectedText = '';
  41. floatingInput = false;
  42. floatingInputValue = '';
  43. }
  44. }
  45. }, 0);
  46. };
  47. const selectAskHandler = () => {
  48. dispatch('select', {
  49. type: 'ask',
  50. content: selectedText,
  51. input: floatingInputValue
  52. });
  53. floatingInput = false;
  54. floatingInputValue = '';
  55. selectedText = '';
  56. // Clear selection
  57. window.getSelection().removeAllRanges();
  58. buttonsContainerElement.style.display = 'none';
  59. };
  60. onMount(() => {
  61. if (floatingButtons) {
  62. contentContainerElement?.addEventListener('mouseup', updateButtonPosition);
  63. document.addEventListener('mouseup', updateButtonPosition);
  64. }
  65. });
  66. onDestroy(() => {
  67. if (floatingButtons) {
  68. contentContainerElement?.removeEventListener('mouseup', updateButtonPosition);
  69. document.removeEventListener('mouseup', updateButtonPosition);
  70. }
  71. });
  72. $: if (floatingButtons) {
  73. if (buttonsContainerElement) {
  74. document.body.appendChild(buttonsContainerElement);
  75. }
  76. }
  77. onDestroy(() => {
  78. if (buttonsContainerElement) {
  79. document.body.removeChild(buttonsContainerElement);
  80. }
  81. });
  82. </script>
  83. <div bind:this={contentContainerElement}>
  84. <Markdown
  85. {id}
  86. {content}
  87. {model}
  88. {save}
  89. on:update={(e) => {
  90. dispatch('update', e.detail);
  91. }}
  92. on:code={(e) => {
  93. const { lang, code } = e.detail;
  94. if (
  95. (['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) &&
  96. !$mobile &&
  97. $chatId
  98. ) {
  99. showArtifacts.set(true);
  100. showControls.set(true);
  101. }
  102. }}
  103. />
  104. </div>
  105. {#if floatingButtons}
  106. <div
  107. bind:this={buttonsContainerElement}
  108. class="absolute rounded-lg mt-1 text-xs z-[9999]"
  109. style="display: none"
  110. >
  111. {#if !floatingInput}
  112. <div
  113. 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"
  114. >
  115. <button
  116. class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
  117. on:click={() => {
  118. selectedText = window.getSelection().toString();
  119. floatingInput = true;
  120. }}
  121. >
  122. <ChatBubble className="size-3 shrink-0" />
  123. <div class="shrink-0">Ask</div>
  124. </button>
  125. <button
  126. class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
  127. on:click={() => {
  128. const selection = window.getSelection();
  129. dispatch('select', {
  130. type: 'explain',
  131. content: selection.toString()
  132. });
  133. // Clear selection
  134. selection.removeAllRanges();
  135. buttonsContainerElement.style.display = 'none';
  136. }}
  137. >
  138. <LightBlub className="size-3 shrink-0" />
  139. <div class="shrink-0">Explain</div>
  140. </button>
  141. </div>
  142. {:else}
  143. <div
  144. 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"
  145. >
  146. <input
  147. type="text"
  148. class="ml-5 bg-transparent outline-none w-full flex-1 text-sm"
  149. placeholder={$i18n.t('Ask a question')}
  150. bind:value={floatingInputValue}
  151. on:keydown={(e) => {
  152. if (e.key === 'Enter') {
  153. selectAskHandler();
  154. }
  155. }}
  156. />
  157. <div class="ml-1 mr-2">
  158. <button
  159. class="{floatingInputValue !== ''
  160. ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
  161. : '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"
  162. on:click={() => {
  163. selectAskHandler();
  164. }}
  165. >
  166. <svg
  167. xmlns="http://www.w3.org/2000/svg"
  168. viewBox="0 0 16 16"
  169. fill="currentColor"
  170. class="size-4"
  171. >
  172. <path
  173. fill-rule="evenodd"
  174. 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"
  175. clip-rule="evenodd"
  176. />
  177. </svg>
  178. </button>
  179. </div>
  180. </div>
  181. {/if}
  182. </div>
  183. {/if}