mirror of
https://github.com/open-webui/open-webui
synced 2025-05-22 13:54:20 +00:00
feat: text select quick actions
This commit is contained in:
parent
1f488a0072
commit
0ad35ffad9
@ -1978,6 +1978,11 @@
|
|||||||
{mergeResponses}
|
{mergeResponses}
|
||||||
{chatActionHandler}
|
{chatActionHandler}
|
||||||
bottomPadding={files.length > 0}
|
bottomPadding={files.length > 0}
|
||||||
|
on:submit={(e) => {
|
||||||
|
if (e.detail) {
|
||||||
|
submitPrompt(e.detail);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
|
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 { toast } from 'svelte-sonner';
|
||||||
import { getChatList, updateChatById } from '$lib/apis/chats';
|
import { getChatList, updateChatById } from '$lib/apis/chats';
|
||||||
@ -382,6 +383,9 @@
|
|||||||
{continueResponse}
|
{continueResponse}
|
||||||
{mergeResponses}
|
{mergeResponses}
|
||||||
{readOnly}
|
{readOnly}
|
||||||
|
on:submit={async (e) => {
|
||||||
|
dispatch('submit', e.detail);
|
||||||
|
}}
|
||||||
on:action={async (e) => {
|
on:action={async (e) => {
|
||||||
if (typeof e.detail === 'string') {
|
if (typeof e.detail === 'string') {
|
||||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||||
|
79
src/lib/components/chat/Messages/ContentRenderer.svelte
Normal file
79
src/lib/components/chat/Messages/ContentRenderer.svelte
Normal 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}
|
@ -76,6 +76,9 @@
|
|||||||
{rateMessage}
|
{rateMessage}
|
||||||
{continueResponse}
|
{continueResponse}
|
||||||
{regenerateResponse}
|
{regenerateResponse}
|
||||||
|
on:submit={async (e) => {
|
||||||
|
dispatch('submit', e.detail);
|
||||||
|
}}
|
||||||
on:action={async (e) => {
|
on:action={async (e) => {
|
||||||
dispatch('action', e.detail);
|
dispatch('action', e.detail);
|
||||||
}}
|
}}
|
||||||
@ -106,6 +109,9 @@
|
|||||||
{continueResponse}
|
{continueResponse}
|
||||||
{regenerateResponse}
|
{regenerateResponse}
|
||||||
{mergeResponses}
|
{mergeResponses}
|
||||||
|
on:submit={async (e) => {
|
||||||
|
dispatch('submit', e.detail);
|
||||||
|
}}
|
||||||
on:action={async (e) => {
|
on:action={async (e) => {
|
||||||
dispatch('action', e.detail);
|
dispatch('action', e.detail);
|
||||||
}}
|
}}
|
||||||
|
@ -215,6 +215,9 @@
|
|||||||
groupedMessageIdsIdx[modelIdx] =
|
groupedMessageIdsIdx[modelIdx] =
|
||||||
groupedMessageIds[modelIdx].messageIds.length - 1;
|
groupedMessageIds[modelIdx].messageIds.length - 1;
|
||||||
}}
|
}}
|
||||||
|
on:submit={async (e) => {
|
||||||
|
dispatch('submit', e.detail);
|
||||||
|
}}
|
||||||
on:action={async (e) => {
|
on:action={async (e) => {
|
||||||
dispatch('action', e.detail);
|
dispatch('action', e.detail);
|
||||||
}}
|
}}
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import type { i18n as i18nType } from 'i18next';
|
import type { i18n as i18nType } from 'i18next';
|
||||||
|
import ContentRenderer from './ContentRenderer.svelte';
|
||||||
|
|
||||||
interface MessageType {
|
interface MessageType {
|
||||||
id: string;
|
id: string;
|
||||||
@ -468,13 +469,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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}
|
{#if message.content === '' && !message.error}
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
{:else if message.content && message.error !== true}
|
{:else if message.content && message.error !== true}
|
||||||
<!-- always show message contents even if there's an error -->
|
<!-- 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 -->
|
<!-- 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}
|
||||||
|
|
||||||
{#if message.error}
|
{#if message.error}
|
||||||
|
19
src/lib/components/icons/LightBlub.svelte
Normal file
19
src/lib/components/icons/LightBlub.svelte
Normal 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>
|
Loading…
Reference in New Issue
Block a user