feat: text select quick actions

This commit is contained in:
Timothy J. Baek 2024-10-05 01:37:39 -07:00
parent 1f488a0072
commit 0ad35ffad9
7 changed files with 130 additions and 3 deletions

View File

@ -1978,6 +1978,11 @@
{mergeResponses}
{chatActionHandler}
bottomPadding={files.length > 0}
on:submit={(e) => {
if (e.detail) {
submitPrompt(e.detail);
}
}}
/>
</div>
</div>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
import { tick, getContext, onMount } from 'svelte';
import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats';
@ -382,6 +383,9 @@
{continueResponse}
{mergeResponses}
{readOnly}
on:submit={async (e) => {
dispatch('submit', e.detail);
}}
on:action={async (e) => {
if (typeof e.detail === 'string') {
await chatActionHandler(chatId, e.detail, message.model, message.id);

View File

@ -0,0 +1,79 @@
<script>
import { onDestroy, onMount, tick, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Markdown from './Markdown.svelte';
import LightBlub from '$lib/components/icons/LightBlub.svelte';
export let id;
export let content;
export let model = null;
export let floatingButtons = true;
let contentContainerElement;
let buttonsContainerElement;
const updateButtonPosition = () => {
setTimeout(async () => {
await tick();
let selection = window.getSelection();
if (selection.toString().trim().length > 0) {
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;
buttonsContainerElement.style.display = 'block';
buttonsContainerElement.style.left = `${left}px`;
buttonsContainerElement.style.top = `${top + 5}px`; // +5 to add some spacing
} else {
buttonsContainerElement.style.display = 'none';
}
}, 0);
};
onMount(() => {
if (floatingButtons) {
contentContainerElement?.addEventListener('mouseup', updateButtonPosition);
}
});
onDestroy(() => {
if (floatingButtons) {
contentContainerElement?.removeEventListener('mouseup', updateButtonPosition);
}
});
</script>
<div bind:this={contentContainerElement}>
<Markdown {id} {content} {model} />
</div>
{#if floatingButtons}
<div
bind:this={buttonsContainerElement}
class="absolute rounded-lg mt-1 p-1 bg-white dark:bg-gray-850 text-xs text-medium shadow-lg"
style="display: none"
>
<button
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-0.5"
on:click={() => {
const selection = window.getSelection();
dispatch('explain', selection.toString());
// Clear selection
selection.removeAllRanges();
buttonsContainerElement.style.display = 'none';
}}
>
<LightBlub className="size-3" />
<div>Explain</div>
</button>
</div>
{/if}

View File

@ -76,6 +76,9 @@
{rateMessage}
{continueResponse}
{regenerateResponse}
on:submit={async (e) => {
dispatch('submit', e.detail);
}}
on:action={async (e) => {
dispatch('action', e.detail);
}}
@ -106,6 +109,9 @@
{continueResponse}
{regenerateResponse}
{mergeResponses}
on:submit={async (e) => {
dispatch('submit', e.detail);
}}
on:action={async (e) => {
dispatch('action', e.detail);
}}

View File

@ -215,6 +215,9 @@
groupedMessageIdsIdx[modelIdx] =
groupedMessageIds[modelIdx].messageIds.length - 1;
}}
on:submit={async (e) => {
dispatch('submit', e.detail);
}}
on:action={async (e) => {
dispatch('action', e.detail);
}}

View File

@ -38,6 +38,7 @@
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
import ContentRenderer from './ContentRenderer.svelte';
interface MessageType {
id: string;
@ -468,13 +469,23 @@
</div>
</div>
{:else}
<div class="w-full flex flex-col">
<div class="w-full flex flex-col relative" id="response-content-container">
{#if message.content === '' && !message.error}
<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 -->
<Markdown id={message.id} content={message.content} {model} />
<ContentRenderer
id={message.id}
content={message.content}
{model}
on:explain={(e) => {
dispatch(
'submit',
`Can you explain this section to me in more detail?\n\n${e.detail}`
);
}}
/>
{/if}
{#if message.error}

View File

@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18"
/>
</svg>