enh: text select copy behaviour
Some checks are pending
Deploy to HuggingFace Spaces / check-secret (push) Waiting to run
Deploy to HuggingFace Spaces / deploy (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda126-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-slim-images (push) Blocked by required conditions
Python CI / Format Backend (push) Waiting to run
Frontend Build / Format & Build Frontend (push) Waiting to run
Frontend Build / Frontend Unit Tests (push) Waiting to run

This commit is contained in:
Timothy Jaeryang Baek 2025-11-13 00:40:29 -05:00
parent 38f45a38cb
commit 2de854fa02

View file

@ -2,7 +2,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher, onDestroy } from 'svelte';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import type { i18n as i18nType, t } from 'i18next'; import type { i18n as i18nType, t } from 'i18next';
@ -152,6 +152,8 @@
export let topPadding = false; export let topPadding = false;
let citationsElement: HTMLDivElement; let citationsElement: HTMLDivElement;
let contentContainerElement: HTMLDivElement;
let buttonsContainerElement: HTMLDivElement; let buttonsContainerElement: HTMLDivElement;
let showDeleteConfirm = false; let showDeleteConfirm = false;
@ -541,24 +543,70 @@
})(); })();
} }
const buttonsWheelHandler = (event: WheelEvent) => {
if (buttonsContainerElement) {
if (buttonsContainerElement.scrollWidth <= buttonsContainerElement.clientWidth) {
// If the container is not scrollable, horizontal scroll
return;
} else {
event.preventDefault();
if (event.deltaY !== 0) {
// Adjust horizontal scroll position based on vertical scroll
buttonsContainerElement.scrollLeft += event.deltaY;
}
}
}
};
const contentCopyHandler = (e) => {
if (contentContainerElement) {
e.preventDefault();
// Get the selected HTML
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const tempDiv = document.createElement('div');
// Remove background, color, and font styles
tempDiv.appendChild(range.cloneContents());
tempDiv.querySelectorAll('table').forEach((table) => {
table.style.borderCollapse = 'collapse';
table.style.width = 'auto';
table.style.tableLayout = 'auto';
});
tempDiv.querySelectorAll('th').forEach((th) => {
th.style.whiteSpace = 'nowrap';
th.style.padding = '4px 8px';
});
// Put cleaned HTML + plain text into clipboard
e.clipboardData.setData('text/html', tempDiv.innerHTML);
e.clipboardData.setData('text/plain', selection.toString());
}
};
onMount(async () => { onMount(async () => {
// console.log('ResponseMessage mounted'); // console.log('ResponseMessage mounted');
await tick(); await tick();
if (buttonsContainerElement) { if (buttonsContainerElement) {
buttonsContainerElement.addEventListener('wheel', function (event) { buttonsContainerElement.addEventListener('wheel', buttonsWheelHandler);
if (buttonsContainerElement.scrollWidth <= buttonsContainerElement.clientWidth) { }
// If the container is not scrollable, horizontal scroll
return;
} else {
event.preventDefault();
if (event.deltaY !== 0) { if (contentContainerElement) {
// Adjust horizontal scroll position based on vertical scroll contentContainerElement.addEventListener('copy', contentCopyHandler);
buttonsContainerElement.scrollLeft += event.deltaY; }
} });
}
}); onDestroy(() => {
if (buttonsContainerElement) {
buttonsContainerElement.removeEventListener('wheel', buttonsWheelHandler);
}
if (contentContainerElement) {
contentContainerElement.removeEventListener('copy', contentCopyHandler);
} }
}); });
</script> </script>
@ -719,72 +767,76 @@
</div> </div>
</div> </div>
</div> </div>
{:else}
<div class="w-full flex flex-col relative" id="response-content-container">
{#if message.content === '' && !message.error && ((model?.info?.meta?.capabilities?.status_updates ?? true) ? (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length === 0 || (message?.statusHistory?.at(-1)?.hidden ?? false) : true)}
<Skeleton />
{:else if message.content && message.error !== true}
<!-- always show message contents even if there's an error -->
<!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content -->
<ContentRenderer
id={`${chatId}-${message.id}`}
messageId={message.id}
{history}
{selectedModels}
content={message.content}
sources={message.sources}
floatingButtons={message?.done &&
!readOnly &&
($settings?.showFloatingActionButtons ?? true)}
save={!readOnly}
preview={!readOnly}
{editCodeBlock}
{topPadding}
done={($settings?.chatFadeStreamingText ?? true)
? (message?.done ?? false)
: true}
{model}
onTaskClick={async (e) => {
console.log(e);
}}
onSourceClick={async (id, idx) => {
console.log(id, idx);
if (citationsElement) {
citationsElement?.showSourceModal(idx - 1);
}
}}
onAddMessages={({ modelId, parentId, messages }) => {
addMessages({ modelId, parentId, messages });
}}
onSave={({ raw, oldContent, newContent }) => {
history.messages[message.id].content = history.messages[
message.id
].content.replace(raw, raw.replace(oldContent, newContent));
updateChat();
}}
/>
{/if}
{#if message?.error}
<Error content={message?.error?.content ?? message.content} />
{/if}
{#if (message?.sources || message?.citations) && (model?.info?.meta?.capabilities?.citations ?? true)}
<Citations
bind:this={citationsElement}
id={message?.id}
sources={message?.sources ?? message?.citations}
{readOnly}
/>
{/if}
{#if message.code_executions}
<CodeExecutions codeExecutions={message.code_executions} />
{/if}
</div>
{/if} {/if}
<div
bind:this={contentContainerElement}
class="w-full flex flex-col relative {edit ? 'hidden' : ''}"
id="response-content-container"
>
{#if message.content === '' && !message.error && ((model?.info?.meta?.capabilities?.status_updates ?? true) ? (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length === 0 || (message?.statusHistory?.at(-1)?.hidden ?? false) : true)}
<Skeleton />
{:else if message.content && message.error !== true}
<!-- always show message contents even if there's an error -->
<!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content -->
<ContentRenderer
id={`${chatId}-${message.id}`}
messageId={message.id}
{history}
{selectedModels}
content={message.content}
sources={message.sources}
floatingButtons={message?.done &&
!readOnly &&
($settings?.showFloatingActionButtons ?? true)}
save={!readOnly}
preview={!readOnly}
{editCodeBlock}
{topPadding}
done={($settings?.chatFadeStreamingText ?? true)
? (message?.done ?? false)
: true}
{model}
onTaskClick={async (e) => {
console.log(e);
}}
onSourceClick={async (id, idx) => {
console.log(id, idx);
if (citationsElement) {
citationsElement?.showSourceModal(idx - 1);
}
}}
onAddMessages={({ modelId, parentId, messages }) => {
addMessages({ modelId, parentId, messages });
}}
onSave={({ raw, oldContent, newContent }) => {
history.messages[message.id].content = history.messages[
message.id
].content.replace(raw, raw.replace(oldContent, newContent));
updateChat();
}}
/>
{/if}
{#if message?.error}
<Error content={message?.error?.content ?? message.content} />
{/if}
{#if (message?.sources || message?.citations) && (model?.info?.meta?.capabilities?.citations ?? true)}
<Citations
bind:this={citationsElement}
id={message?.id}
sources={message?.sources ?? message?.citations}
{readOnly}
/>
{/if}
{#if message.code_executions}
<CodeExecutions codeExecutions={message.code_executions} />
{/if}
</div>
</div> </div>
</div> </div>