|
<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) => { |
|
if ( |
|
!contentContainerElement?.contains(event.target) && |
|
!buttonsContainerElement?.contains(event.target) |
|
) { |
|
closeFloatingButtons(); |
|
return; |
|
} |
|
|
|
setTimeout(async () => { |
|
await tick(); |
|
|
|
if (!contentContainerElement?.contains(event.target)) 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(); |
|
|
|
|
|
const top = rect.bottom - parentRect.top; |
|
const left = rect.left - parentRect.left; |
|
|
|
if (buttonsContainerElement) { |
|
buttonsContainerElement.style.display = 'block'; |
|
|
|
|
|
const spaceOnRight = parentRect.width - (left + buttonsContainerElement.offsetWidth); |
|
|
|
let thirdScreenWidth = window.innerWidth / 3; |
|
|
|
if (spaceOnRight < thirdScreenWidth) { |
|
const right = parentRect.right - rect.right; |
|
buttonsContainerElement.style.right = `${right}px`; |
|
buttonsContainerElement.style.left = 'auto'; |
|
} else { |
|
|
|
buttonsContainerElement.style.left = `${left}px`; |
|
buttonsContainerElement.style.right = 'auto'; |
|
} |
|
|
|
buttonsContainerElement.style.top = `${top + 5}px`; |
|
} |
|
} 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 = ''; |
|
|
|
|
|
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} |
|
|