Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
inference-widgets
/
packages
/widgets
/src
/lib
/components
/InferenceWidget
/shared
/WidgetTextarea
/WidgetTextarea.svelte
<script lang="ts"> | |
import { createEventDispatcher, tick } from "svelte"; | |
import { delay, onCmdEnter } from "../../../../utils/ViewUtils.js"; | |
import WidgetLabel from "../WidgetLabel/WidgetLabel.svelte"; | |
import LogInPopover from "../../../LogInPopover/LogInPopover.svelte"; | |
import { isLoggedIn } from "../../stores.js"; | |
export let label: string = ""; | |
export let placeholder: string = "Your sentence here..."; | |
export let value: string; | |
export let isLoading = false; | |
export let isDisabled = false; | |
export let size: "small" | "big" = "small"; | |
let containerSpanEl: HTMLSpanElement; | |
let isOnFocus = false; | |
let popOverOpen = false; | |
const typingEffectSpeedMs = 12; | |
const classNamesInput = "whitespace-pre-wrap inline font-normal text-black dark:text-white"; | |
const classNamesOutput = "whitespace-pre-wrap inline text-blue-600 dark:text-blue-400"; | |
const dispatch = createEventDispatcher<{ cmdEnter: void }>(); | |
export async function renderTextOutput(outputTxt: string, typingEffect = true): Promise<void> { | |
const spanEl = document.createElement("span"); | |
spanEl.contentEditable = isDisabled ? "false" : "true"; | |
spanEl.className = classNamesOutput; | |
containerSpanEl?.appendChild(spanEl); | |
await tick(); | |
// fix Chrome bug that adds `<br>` els on contentedtiable el | |
const brElts = containerSpanEl?.querySelectorAll("br"); | |
for (const brEl of brElts) { | |
brEl.remove(); | |
} | |
await tick(); | |
// split on whitespace or any other character to correctly render newlines \n | |
if (typingEffect) { | |
for (const char of outputTxt.split(/(\s|.)/g)) { | |
await delay(typingEffectSpeedMs); | |
spanEl.textContent += char; | |
if (isOnFocus) { | |
moveCaretToEnd(); | |
} | |
} | |
} else { | |
spanEl.textContent = outputTxt; | |
} | |
updateInnerTextValue(); | |
} | |
function moveCaretToEnd() { | |
if (containerSpanEl) { | |
const range = document.createRange(); | |
range.selectNodeContents(containerSpanEl); | |
range.collapse(false); | |
const selection = window.getSelection(); | |
selection?.removeAllRanges(); | |
selection?.addRange(range); | |
} | |
} | |
// handle FireFox contenteditable paste bug | |
function handlePaste(e: ClipboardEvent) { | |
if (isLoading) { | |
return e.preventDefault(); | |
} | |
const copiedTxt = e.clipboardData?.getData("text/plain"); | |
const selection = window.getSelection(); | |
if (selection?.rangeCount && !!copiedTxt?.length) { | |
const range = selection.getRangeAt(0); | |
range.deleteContents(); | |
const spanEl = document.createElement("span"); | |
spanEl.contentEditable = "true"; | |
spanEl.className = classNamesInput; | |
spanEl.textContent = copiedTxt; | |
range.insertNode(spanEl); | |
} | |
window.getSelection()?.collapseToEnd(); | |
updateInnerTextValue(); | |
} | |
function updateInnerTextValue() { | |
value = containerSpanEl?.textContent ?? ""; | |
} | |
function onFocus() { | |
isOnFocus = true; | |
moveCaretToEnd(); | |
} | |
export function setValue(text: string): void { | |
containerSpanEl.textContent = text; | |
updateInnerTextValue(); | |
} | |
</script> | |
<LogInPopover bind:open={popOverOpen}> | |
<WidgetLabel {label}> | |
<svelte:fragment slot="after"> | |
<!-- `whitespace-pre-wrap inline-block` are needed to get correct newlines from `el.textContent` on Chrome --> | |
<span | |
class="{label ? 'mt-1.5' : ''} block w-full resize-y overflow-auto py-2 px-3 {size === 'small' | |
? 'min-h-[42px]' | |
: 'min-h-[144px]'} inline-block max-h-[500px] whitespace-pre-wrap rounded-lg border border-gray-200 shadow-inner outline-none focus:shadow-inner focus:ring focus:ring-blue-200 dark:bg-gray-925" | |
role="textbox" | |
style="--placeholder: '{isDisabled ? '' : placeholder}'" | |
spellcheck="false" | |
dir="auto" | |
contenteditable | |
class:pointer-events-none={isLoading || isDisabled} | |
use:onCmdEnter={{ disabled: isLoading || isDisabled }} | |
on:cmdEnter={() => { | |
if (!$isLoggedIn) { | |
popOverOpen = true; | |
return; | |
} | |
dispatch("cmdEnter"); | |
}} | |
bind:this={containerSpanEl} | |
on:paste|preventDefault={handlePaste} | |
on:input={updateInnerTextValue} | |
on:focus={onFocus} | |
on:blur={() => (isOnFocus = false)} | |
/> | |
</svelte:fragment> | |
</WidgetLabel> | |
</LogInPopover> | |
<style> | |
span[contenteditable]:empty::before { | |
content: var(--placeholder); | |
color: rgba(156, 163, 175); | |
} | |
</style> | |