This commit is contained in:
Timothy Jaeryang Baek 2025-08-07 00:12:24 +04:00
parent 4797799400
commit 353ecda084
2 changed files with 75 additions and 129 deletions

View file

@ -17,14 +17,15 @@
export let id = ''; export let id = '';
export let model = null; export let model = null;
export let messages = []; export let messages = [];
export let onAdd = () => {}; export let onAdd = (e) => {};
let floatingInput = false; let floatingInput = false;
let selectedAction = null;
let selectedText = ''; let selectedText = '';
let floatingInputValue = ''; let floatingInputValue = '';
let prompt = ''; let content = '';
let responseContent = null; let responseContent = null;
let responseDone = false; let responseDone = false;
let controller = null; let controller = null;
@ -42,108 +43,51 @@
} }
}; };
const askHandler = async () => { const ACTIONS = [
if (!model) {
toast.error('Model not selected');
return;
}
prompt = [
// Blockquote each line of the selected text
...selectedText.split('\n').map((line) => `> ${line}`),
'',
// Then your question
floatingInputValue
].join('\n');
floatingInputValue = '';
responseContent = '';
let res;
[res, controller] = await chatCompletion(localStorage.token, {
model: model,
messages: [
...messages,
{ {
role: 'user', id: 'ask',
content: prompt label: $i18n.t('Ask'),
icon: ChatBubble,
input: true,
prompt: `{{SELECTED_CONTENT}}\n\n\n{{INPUT_CONTENT}}`
},
{
id: 'explain',
label: $i18n.t('Explain'),
icon: LightBulb,
prompt: `{{SELECTED_CONTENT}}\n\n\nExplain`
} }
].map((message) => ({ ];
role: message.role,
content: message.content
})),
stream: true // Enable streaming
});
if (res && res.ok) { const actionHandler = async (actionId) => {
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
try {
await processStream();
} catch (e) {
if (e.name !== 'AbortError') {
console.error(e);
}
}
} else {
toast.error('An error occurred while fetching the explanation');
}
};
const explainHandler = async () => {
if (!model) { if (!model) {
toast.error('Model not selected'); toast.error('Model not selected');
return; return;
} }
const quotedText = selectedText
let selectedContent = selectedText
.split('\n') .split('\n')
.map((line) => `> ${line}`) .map((line) => `> ${line}`)
.join('\n'); .join('\n');
prompt = `${quotedText}\n\nExplain`;
let selectedAction = ACTIONS.find((action) => action.id === actionId);
if (!selectedAction) {
toast.error('Action not found');
return;
}
let prompt = selectedAction?.prompt ?? '';
if (selectedAction.input) {
prompt = prompt.replace('{{INPUT_CONTENT}}', floatingInputValue);
floatingInputValue = '';
}
prompt = prompt.replace('{{CONTENT}}', selectedText);
prompt = prompt.replace('{{SELECTED_CONTENT}}', selectedContent);
content = prompt;
responseContent = ''; responseContent = '';
let res; let res;
[res, controller] = await chatCompletion(localStorage.token, { [res, controller] = await chatCompletion(localStorage.token, {
model: model, model: model,
@ -151,7 +95,7 @@
...messages, ...messages,
{ {
role: 'user', role: 'user',
content: prompt content: content
} }
].map((message) => ({ ].map((message) => ({
role: message.role, role: message.role,
@ -223,7 +167,7 @@
const messages = [ const messages = [
{ {
role: 'user', role: 'user',
content: prompt content: content
}, },
{ {
role: 'assistant', role: 'assistant',
@ -239,6 +183,12 @@
}; };
export const closeHandler = () => { export const closeHandler = () => {
if (controller) {
controller.abort();
}
selectedAction = null;
selectedText = '';
responseContent = null; responseContent = null;
responseDone = false; responseDone = false;
floatingInput = false; floatingInput = false;
@ -262,11 +212,16 @@
<div <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" 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"
> >
{#each ACTIONS as action}
<button <button
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit" 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 () => { on:click={async () => {
selectedText = window.getSelection().toString(); selectedText = window.getSelection().toString();
selectedAction = action;
if (action.input) {
floatingInput = true; floatingInput = true;
floatingInputValue = '';
await tick(); await tick();
setTimeout(() => { setTimeout(() => {
@ -275,23 +230,15 @@
input.focus(); input.focus();
} }
}, 0); }, 0);
} else {
actionHandler(action.id);
}
}} }}
> >
<ChatBubble className="size-3 shrink-0" /> <svelte:component this={action.icon} className="size-3 shrink-0" />
<div class="shrink-0">{action.label}</div>
<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();
}}
>
<LightBulb className="size-3 shrink-0" />
<div class="shrink-0">{$i18n.t('Explain')}</div>
</button> </button>
{/each}
</div> </div>
{:else} {:else}
<div <div
@ -305,7 +252,7 @@
bind:value={floatingInputValue} bind:value={floatingInputValue}
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
askHandler(); actionHandler(selectedAction?.id);
} }
}} }}
/> />
@ -316,7 +263,7 @@
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' ? '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" : '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={() => { on:click={() => {
askHandler(); actionHandler(selectedAction?.id);
}} }}
> >
<svg <svg
@ -341,7 +288,7 @@
class="bg-gray-50/50 dark:bg-gray-800 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full" 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"> <div class="font-medium">
<Markdown id={`${id}-float-prompt`} content={prompt} /> <Markdown id={`${id}-float-prompt`} {content} />
</div> </div>
</div> </div>
@ -349,7 +296,7 @@
class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full" 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"> <div class=" max-h-80 overflow-y-auto w-full markdown-prose-xs" id="response-container">
{#if responseContent.trim() === ''} {#if !responseContent || responseContent?.trim() === ''}
<Skeleton size="sm" /> <Skeleton size="sm" />
{:else} {:else}
<Markdown id={`${id}-float-response`} content={responseContent} /> <Markdown id={`${id}-float-response`} content={responseContent} />

View file

@ -34,7 +34,6 @@
export let onAddMessages = (e) => {}; export let onAddMessages = (e) => {};
let contentContainerElement; let contentContainerElement;
let floatingButtonsElement; let floatingButtonsElement;
const updateButtonPosition = (event) => { const updateButtonPosition = (event) => {