mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
refac: floating buttons
This commit is contained in:
parent
d6f0c77c34
commit
37ce88e744
@ -56,6 +56,10 @@ math {
|
|||||||
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
@apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-prose-xs {
|
||||||
|
@apply text-xs prose dark:prose-invert prose-headings:font-semibold prose-hr:my-0 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown a {
|
.markdown a {
|
||||||
@apply underline;
|
@apply underline;
|
||||||
}
|
}
|
||||||
|
@ -273,6 +273,38 @@ export const verifyOpenAIConnection = async (
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const chatCompletion = async (
|
||||||
|
token: string = '',
|
||||||
|
body: object,
|
||||||
|
url: string = OPENAI_API_BASE_URL
|
||||||
|
): Promise<[Response | null, AbortController]> => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${url}/chat/completions`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
error = err;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [res, controller];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const generateOpenAIChatCompletion = async (
|
export const generateOpenAIChatCompletion = async (
|
||||||
token: string = '',
|
token: string = '',
|
||||||
body: object,
|
body: object,
|
||||||
|
303
src/lib/components/chat/ContentRenderer/FloatingButtons.svelte
Normal file
303
src/lib/components/chat/ContentRenderer/FloatingButtons.svelte
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
import { getContext, tick } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { chatCompletion } from '$lib/apis/openai';
|
||||||
|
|
||||||
|
import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
|
||||||
|
import LightBlub from '$lib/components/icons/LightBlub.svelte';
|
||||||
|
import Markdown from '../Messages/Markdown.svelte';
|
||||||
|
import Skeleton from '../Messages/Skeleton.svelte';
|
||||||
|
|
||||||
|
export let id = '';
|
||||||
|
export let model = null;
|
||||||
|
export let messages = [];
|
||||||
|
export let onAdd = () => {};
|
||||||
|
|
||||||
|
let floatingInput = false;
|
||||||
|
|
||||||
|
let selectedText = '';
|
||||||
|
let floatingInputValue = '';
|
||||||
|
|
||||||
|
let prompt = '';
|
||||||
|
let responseContent = null;
|
||||||
|
|
||||||
|
const askHandler = async () => {
|
||||||
|
if (!model) {
|
||||||
|
toast.error('Model not selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prompt = `${floatingInputValue}\n\`\`\`\n${selectedText}\n\`\`\``;
|
||||||
|
floatingInputValue = '';
|
||||||
|
|
||||||
|
responseContent = '';
|
||||||
|
const [res, controller] = await chatCompletion(localStorage.token, {
|
||||||
|
model: model,
|
||||||
|
messages: [
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}
|
||||||
|
],
|
||||||
|
stream: true // Enable streaming
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.ok) {
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
const processStream = async () => {
|
||||||
|
while (true) {
|
||||||
|
// Read data chunks from the response stream
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the received chunk
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Process lines within the chunk
|
||||||
|
const lines = chunk.split('\n').filter((line) => line.trim() !== '');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
if (line.startsWith('data: [DONE]')) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Parse the JSON chunk
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(line.slice(6));
|
||||||
|
|
||||||
|
// Append the `content` field from the "choices" object
|
||||||
|
if (data.choices && data.choices[0]?.delta?.content) {
|
||||||
|
responseContent += data.choices[0].delta.content;
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
const responseContainer = document.getElementById('response-container');
|
||||||
|
responseContainer.scrollTop = responseContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process the stream in the background
|
||||||
|
await processStream();
|
||||||
|
} else {
|
||||||
|
toast.error('An error occurred while fetching the explanation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const explainHandler = async () => {
|
||||||
|
if (!model) {
|
||||||
|
toast.error('Model not selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prompt = `Explain this section to me in more detail\n\n\`\`\`\n${selectedText}\n\`\`\``;
|
||||||
|
|
||||||
|
responseContent = '';
|
||||||
|
const [res, controller] = await chatCompletion(localStorage.token, {
|
||||||
|
model: model,
|
||||||
|
messages: [
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}
|
||||||
|
],
|
||||||
|
stream: true // Enable streaming
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.ok) {
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
const processStream = async () => {
|
||||||
|
while (true) {
|
||||||
|
// Read data chunks from the response stream
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the received chunk
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Process lines within the chunk
|
||||||
|
const lines = chunk.split('\n').filter((line) => line.trim() !== '');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
if (line.startsWith('data: [DONE]')) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Parse the JSON chunk
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(line.slice(6));
|
||||||
|
|
||||||
|
// Append the `content` field from the "choices" object
|
||||||
|
if (data.choices && data.choices[0]?.delta?.content) {
|
||||||
|
responseContent += data.choices[0].delta.content;
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
const responseContainer = document.getElementById('response-container');
|
||||||
|
responseContainer.scrollTop = responseContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process the stream in the background
|
||||||
|
await processStream();
|
||||||
|
} else {
|
||||||
|
toast.error('An error occurred while fetching the explanation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addHandler = async () => {
|
||||||
|
newMessages = [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: responseContent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
responseContent = null;
|
||||||
|
|
||||||
|
onAdd();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const closeHandler = () => {
|
||||||
|
responseContent = null;
|
||||||
|
floatingInput = false;
|
||||||
|
floatingInputValue = '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id={`floating-buttons-${id}`}
|
||||||
|
class="absolute rounded-lg mt-1 text-xs z-[9999]"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
{#if responseContent === null}
|
||||||
|
{#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={async () => {
|
||||||
|
selectedText = window.getSelection().toString();
|
||||||
|
floatingInput = true;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = document.getElementById('floating-message-input');
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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={() => {
|
||||||
|
selectedText = window.getSelection().toString();
|
||||||
|
explainHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
id="floating-message-input"
|
||||||
|
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') {
|
||||||
|
askHandler();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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={() => {
|
||||||
|
askHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
||||||
|
{:else}
|
||||||
|
<div class="bg-white dark:bg-gray-850 dark:text-gray-100 rounded-xl shadow-xl w-80 max-w-full">
|
||||||
|
<div
|
||||||
|
class="bg-gray-50/50 dark:bg-gray-800 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full"
|
||||||
|
>
|
||||||
|
<div class="font-medium">
|
||||||
|
<Markdown id={`${id}-float-prompt`} content={prompt} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full"
|
||||||
|
>
|
||||||
|
<div class=" max-h-80 overflow-y-auto w-full markdown-prose-xs" id="response-container">
|
||||||
|
{#if responseContent.trim() === ''}
|
||||||
|
<Skeleton size="sm" />
|
||||||
|
{:else}
|
||||||
|
<Markdown id={`${id}-float-response`} content={responseContent} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
@ -4,13 +4,13 @@
|
|||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import Markdown from './Markdown.svelte';
|
import Markdown from './Markdown.svelte';
|
||||||
import LightBlub from '$lib/components/icons/LightBlub.svelte';
|
|
||||||
import { chatId, mobile, showArtifacts, showControls, showOverview } from '$lib/stores';
|
import { chatId, mobile, showArtifacts, showControls, showOverview } from '$lib/stores';
|
||||||
import ChatBubble from '$lib/components/icons/ChatBubble.svelte';
|
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
|
||||||
import { stringify } from 'postcss';
|
import { createMessagesList } from '$lib/utils';
|
||||||
|
|
||||||
export let id;
|
export let id;
|
||||||
export let content;
|
export let content;
|
||||||
|
export let history;
|
||||||
export let model = null;
|
export let model = null;
|
||||||
export let sources = null;
|
export let sources = null;
|
||||||
|
|
||||||
@ -19,13 +19,11 @@
|
|||||||
export let onSourceClick = () => {};
|
export let onSourceClick = () => {};
|
||||||
|
|
||||||
let contentContainerElement;
|
let contentContainerElement;
|
||||||
let buttonsContainerElement;
|
|
||||||
|
|
||||||
let selectedText = '';
|
let floatingButtonsElement;
|
||||||
let floatingInput = false;
|
|
||||||
let floatingInputValue = '';
|
|
||||||
|
|
||||||
const updateButtonPosition = (event) => {
|
const updateButtonPosition = (event) => {
|
||||||
|
const buttonsContainerElement = document.getElementById(`floating-buttons-${id}`);
|
||||||
if (
|
if (
|
||||||
!contentContainerElement?.contains(event.target) &&
|
!contentContainerElement?.contains(event.target) &&
|
||||||
!buttonsContainerElement?.contains(event.target)
|
!buttonsContainerElement?.contains(event.target)
|
||||||
@ -42,7 +40,6 @@
|
|||||||
let selection = window.getSelection();
|
let selection = window.getSelection();
|
||||||
|
|
||||||
if (selection.toString().trim().length > 0) {
|
if (selection.toString().trim().length > 0) {
|
||||||
floatingInput = false;
|
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
const rect = range.getBoundingClientRect();
|
const rect = range.getBoundingClientRect();
|
||||||
|
|
||||||
@ -56,11 +53,10 @@
|
|||||||
buttonsContainerElement.style.display = 'block';
|
buttonsContainerElement.style.display = 'block';
|
||||||
|
|
||||||
// Calculate space available on the right
|
// Calculate space available on the right
|
||||||
const spaceOnRight = parentRect.width - (left + buttonsContainerElement.offsetWidth);
|
const spaceOnRight = parentRect.width - left;
|
||||||
|
let halfScreenWidth = window.innerWidth / 2;
|
||||||
|
|
||||||
let thirdScreenWidth = window.innerWidth / 3;
|
if (spaceOnRight < halfScreenWidth) {
|
||||||
|
|
||||||
if (spaceOnRight < thirdScreenWidth) {
|
|
||||||
const right = parentRect.right - rect.right;
|
const right = parentRect.right - rect.right;
|
||||||
buttonsContainerElement.style.right = `${right}px`;
|
buttonsContainerElement.style.right = `${right}px`;
|
||||||
buttonsContainerElement.style.left = 'auto'; // Reset left
|
buttonsContainerElement.style.left = 'auto'; // Reset left
|
||||||
@ -69,7 +65,6 @@
|
|||||||
buttonsContainerElement.style.left = `${left}px`;
|
buttonsContainerElement.style.left = `${left}px`;
|
||||||
buttonsContainerElement.style.right = 'auto'; // Reset right
|
buttonsContainerElement.style.right = 'auto'; // Reset right
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonsContainerElement.style.top = `${top + 5}px`; // +5 to add some spacing
|
buttonsContainerElement.style.top = `${top + 5}px`; // +5 to add some spacing
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -79,28 +74,14 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeFloatingButtons = () => {
|
const closeFloatingButtons = () => {
|
||||||
|
const buttonsContainerElement = document.getElementById(`floating-buttons-${id}`);
|
||||||
if (buttonsContainerElement) {
|
if (buttonsContainerElement) {
|
||||||
buttonsContainerElement.style.display = 'none';
|
buttonsContainerElement.style.display = 'none';
|
||||||
selectedText = '';
|
|
||||||
floatingInput = false;
|
|
||||||
floatingInputValue = '';
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const selectAskHandler = () => {
|
if (floatingButtonsElement) {
|
||||||
dispatch('select', {
|
floatingButtonsElement.closeHandler();
|
||||||
type: 'ask',
|
}
|
||||||
content: selectedText,
|
|
||||||
input: floatingInputValue
|
|
||||||
});
|
|
||||||
|
|
||||||
floatingInput = false;
|
|
||||||
floatingInputValue = '';
|
|
||||||
selectedText = '';
|
|
||||||
|
|
||||||
// Clear selection
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
buttonsContainerElement.style.display = 'none';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const keydownHandler = (e) => {
|
const keydownHandler = (e) => {
|
||||||
@ -176,86 +157,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if floatingButtons}
|
{#if floatingButtons && model}
|
||||||
<div
|
<FloatingButtons
|
||||||
bind:this={buttonsContainerElement}
|
bind:this={floatingButtonsElement}
|
||||||
class="absolute rounded-lg mt-1 text-xs z-[9999]"
|
{id}
|
||||||
style="display: none"
|
model={model?.id}
|
||||||
>
|
messages={createMessagesList(history, id)}
|
||||||
{#if !floatingInput}
|
onSave={() => {
|
||||||
<div
|
closeFloatingButtons();
|
||||||
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}
|
{/if}
|
||||||
|
@ -620,6 +620,7 @@
|
|||||||
<!-- 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 -->
|
||||||
<ContentRenderer
|
<ContentRenderer
|
||||||
id={message.id}
|
id={message.id}
|
||||||
|
{history}
|
||||||
content={message.content}
|
content={message.content}
|
||||||
sources={message.sources}
|
sources={message.sources}
|
||||||
floatingButtons={message?.done}
|
floatingButtons={message?.done}
|
||||||
|
@ -1,19 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let size = 'md';
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="w-full mt-2 mb-2">
|
<div class="w-full mt-2 mb-2">
|
||||||
<div class="animate-pulse flex w-full">
|
<div class="animate-pulse flex w-full">
|
||||||
<div class="space-y-2 w-full">
|
<div class="{size === 'md' ? 'space-y-2' : 'space-y-1.5'} w-full">
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
|
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded mr-14" />
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-3 gap-4">
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" />
|
<div
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" />
|
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-2"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-4 gap-4">
|
<div class="grid grid-cols-4 gap-4">
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" />
|
<div
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" />
|
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1"
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4" />
|
/>
|
||||||
|
<div
|
||||||
|
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-2"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="{size === 'md'
|
||||||
|
? 'h-2'
|
||||||
|
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded" />
|
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user