/
Netrunners
/
Millenium_UI
Обзор
Документация
Войти
/
Netrunners
/
Millenium_UI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/lib/components/chat/ContentRenderer/FloatingButtons.svelte
340 строк
9 KB
Валерий Конюхов
first_commit
08 июл 2025, 22:20
08 июл 2025, 22:20
a4e8dc9
Код
Авторство
О чём код?
<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; let responseDone = false; const autoScroll = async () => { // Scroll to bottom only if the scroll is at the bottom give 50px buffer const responseContainer = document.getElementById('response-container'); if ( responseContainer.scrollHeight - responseContainer.clientHeight <= responseContainer.scrollTop + 50 ) { responseContainer.scrollTop = responseContainer.scrollHeight; } }; 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 } ].map((message) => ({ role: message.role, content: message.content })), 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]')) { responseDone = true; await tick(); autoScroll(); 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; autoScroll(); } } 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; } const explainText = $i18n.t('Explain this section to me in more detail'); prompt = `${explainText}\n\n\`\`\`\n${selectedText}\n\`\`\``; responseContent = ''; const [res, controller] = await chatCompletion(localStorage.token, { model: model, messages: [ ...messages, { role: 'user', content: prompt } ].map((message) => ({ role: message.role, content: message.content })), 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]')) { responseDone = true; await tick(); autoScroll(); 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; autoScroll(); } } 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 () => { const messages = [ { role: 'user', content: prompt }, { role: 'assistant', content: responseContent } ]; onAdd({ modelId: model, parentId: id, messages: messages }); }; export const closeHandler = () => { responseContent = null; responseDone = false; 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-sm 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">{$i18n.t('Ask')}</div> </button> <button class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm 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">{$i18n.t('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-850 w-72 rounded-full shadow-xl" > <input type="text" id="floating-message-input" class="ml-5 bg-transparent outline-hidden 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} {#if responseDone} <div class="flex justify-end pt-3 text-sm font-medium"> <button class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" on:click={addHandler} > {$i18n.t('Add')} </button> </div> {/if} </div> </div> </div> {/if} </div>