123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- <script>
- import { onDestroy, onMount, tick, getContext, createEventDispatcher } from 'svelte';
- const i18n = getContext('i18n');
- const dispatch = createEventDispatcher();
- import Markdown from './Markdown.svelte';
- import LightBlub from '$lib/components/icons/LightBlub.svelte';
- import { chatId, mobile, showArtifacts, showControls, showOverview } from '$lib/stores';
- import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
- export let id;
- export let content;
- export let model = null;
- export let save = false;
- export let floatingButtons = true;
- let contentContainerElement;
- let buttonsContainerElement;
- let selectedText = '';
- let floatingInput = false;
- let floatingInputValue = '';
- const updateButtonPosition = (event) => {
- setTimeout(async () => {
- await tick();
- // Check if the event target is within the content container
- if (!contentContainerElement?.contains(event.target)) {
- closeFloatingButtons();
- return;
- }
- let selection = window.getSelection();
- if (selection.toString().trim().length > 0) {
- floatingInput = false;
- const range = selection.getRangeAt(0);
- const rect = range.getBoundingClientRect();
- const parentRect = contentContainerElement.getBoundingClientRect();
- // Adjust based on parent rect
- const top = rect.bottom - parentRect.top;
- const left = rect.left - parentRect.left;
- if (buttonsContainerElement) {
- buttonsContainerElement.style.display = 'block';
- // Calculate space available on the right
- const spaceOnRight = parentRect.width - (left + buttonsContainerElement.offsetWidth);
- if (spaceOnRight < 0) {
- // Not enough space on the right, position using 'right'
- const right = parentRect.right - rect.right;
- buttonsContainerElement.style.right = `${right}px`;
- buttonsContainerElement.style.left = 'auto'; // Reset left
- } else {
- // Enough space, position using 'left'
- buttonsContainerElement.style.left = `${left}px`;
- buttonsContainerElement.style.right = 'auto'; // Reset right
- }
- buttonsContainerElement.style.top = `${top + 5}px`; // +5 to add some spacing
- }
- } else {
- closeFloatingButtons();
- }
- }, 0);
- };
- const closeFloatingButtons = () => {
- if (buttonsContainerElement) {
- buttonsContainerElement.style.display = 'none';
- selectedText = '';
- floatingInput = false;
- floatingInputValue = '';
- }
- };
- const selectAskHandler = () => {
- dispatch('select', {
- type: 'ask',
- content: selectedText,
- input: floatingInputValue
- });
- floatingInput = false;
- floatingInputValue = '';
- selectedText = '';
- // Clear selection
- window.getSelection().removeAllRanges();
- buttonsContainerElement.style.display = 'none';
- };
- const keydownHandler = (e) => {
- if (e.key === 'Escape') {
- closeFloatingButtons();
- }
- };
- onMount(() => {
- if (floatingButtons) {
- contentContainerElement?.addEventListener('mouseup', updateButtonPosition);
- document.addEventListener('mouseup', updateButtonPosition);
- document.addEventListener('keydown', keydownHandler);
- }
- });
- onDestroy(() => {
- if (floatingButtons) {
- contentContainerElement?.removeEventListener('mouseup', updateButtonPosition);
- document.removeEventListener('mouseup', updateButtonPosition);
- document.removeEventListener('keydown', keydownHandler);
- }
- });
- </script>
- <div bind:this={contentContainerElement}>
- <Markdown
- {id}
- {content}
- {model}
- {save}
- on:update={(e) => {
- dispatch('update', e.detail);
- }}
- on:code={(e) => {
- const { lang, code } = e.detail;
- if (
- (['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) &&
- !$mobile &&
- $chatId
- ) {
- showArtifacts.set(true);
- showControls.set(true);
- }
- }}
- />
- </div>
- {#if floatingButtons}
- <div
- bind:this={buttonsContainerElement}
- class="absolute rounded-lg mt-1 text-xs z-[9999]"
- style="display: none"
- >
- {#if !floatingInput}
- <div
- 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"
- >
- <button
- class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
- on:click={() => {
- selectedText = window.getSelection().toString();
- floatingInput = true;
- }}
- >
- <ChatBubble className="size-3 shrink-0" />
- <div class="shrink-0">Ask</div>
- </button>
- <button
- class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
- on:click={() => {
- const selection = window.getSelection();
- dispatch('select', {
- type: 'explain',
- content: selection.toString()
- });
- // Clear selection
- selection.removeAllRanges();
- buttonsContainerElement.style.display = 'none';
- }}
- >
- <LightBlub className="size-3 shrink-0" />
- <div class="shrink-0">Explain</div>
- </button>
- </div>
- {:else}
- <div
- 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"
- >
- <input
- type="text"
- class="ml-5 bg-transparent outline-none w-full flex-1 text-sm"
- placeholder={$i18n.t('Ask a question')}
- bind:value={floatingInputValue}
- on:keydown={(e) => {
- if (e.key === 'Enter') {
- selectAskHandler();
- }
- }}
- />
- <div class="ml-1 mr-2">
- <button
- class="{floatingInputValue !== ''
- ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
- : '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"
- on:click={() => {
- selectAskHandler();
- }}
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- class="size-4"
- >
- <path
- fill-rule="evenodd"
- 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"
- clip-rule="evenodd"
- />
- </svg>
- </button>
- </div>
- </div>
- {/if}
- </div>
- {/if}
|