mirror of
https://github.com/open-webui/open-webui
synced 2025-01-18 00:30:51 +00:00
refac: input commands
This commit is contained in:
parent
64c0157271
commit
591962d906
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
||||
import {
|
||||
type Model,
|
||||
mobile,
|
||||
@ -12,15 +13,9 @@
|
||||
tools,
|
||||
user as _user
|
||||
} from '$lib/stores';
|
||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||
|
||||
import {
|
||||
processDocToVectorDB,
|
||||
uploadDocToVectorDB,
|
||||
uploadWebToVectorDB,
|
||||
uploadYoutubeTranscriptionToVectorDB
|
||||
} from '$lib/apis/rag';
|
||||
|
||||
import { blobToFile, findWordIndices } from '$lib/utils';
|
||||
import { processDocToVectorDB } from '$lib/apis/rag';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import { uploadFile } from '$lib/apis/files';
|
||||
import {
|
||||
SUPPORTED_FILE_TYPE,
|
||||
@ -29,19 +24,14 @@
|
||||
WEBUI_API_BASE_URL
|
||||
} from '$lib/constants';
|
||||
|
||||
import Prompts from './MessageInput/PromptCommands.svelte';
|
||||
import Suggestions from './MessageInput/Suggestions.svelte';
|
||||
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
|
||||
import Documents from './MessageInput/Documents.svelte';
|
||||
import Models from './MessageInput/Models.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||
import Headphone from '../icons/Headphone.svelte';
|
||||
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import FileItem from '../common/FileItem.svelte';
|
||||
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||
import Commands from './MessageInput/Commands.svelte';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@ -60,9 +50,7 @@
|
||||
let chatTextAreaElement: HTMLTextAreaElement;
|
||||
let filesInputElement;
|
||||
|
||||
let promptsElement;
|
||||
let documentsElement;
|
||||
let modelsElement;
|
||||
let commandsElement;
|
||||
|
||||
let inputFiles;
|
||||
let dragged = false;
|
||||
@ -180,62 +168,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const uploadWeb = async (url) => {
|
||||
console.log(url);
|
||||
|
||||
const doc = {
|
||||
type: 'doc',
|
||||
name: url,
|
||||
collection_name: '',
|
||||
status: false,
|
||||
url: url,
|
||||
error: ''
|
||||
};
|
||||
|
||||
try {
|
||||
files = [...files, doc];
|
||||
const res = await uploadWebToVectorDB(localStorage.token, '', url);
|
||||
|
||||
if (res) {
|
||||
doc.status = 'processed';
|
||||
doc.collection_name = res.collection_name;
|
||||
files = files;
|
||||
}
|
||||
} catch (e) {
|
||||
// Remove the failed doc from the files array
|
||||
files = files.filter((f) => f.name !== url);
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadYoutubeTranscription = async (url) => {
|
||||
console.log(url);
|
||||
|
||||
const doc = {
|
||||
type: 'doc',
|
||||
name: url,
|
||||
collection_name: '',
|
||||
status: false,
|
||||
url: url,
|
||||
error: ''
|
||||
};
|
||||
|
||||
try {
|
||||
files = [...files, doc];
|
||||
const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
|
||||
|
||||
if (res) {
|
||||
doc.status = 'processed';
|
||||
doc.collection_name = res.collection_name;
|
||||
files = files;
|
||||
}
|
||||
} catch (e) {
|
||||
// Remove the failed doc from the files array
|
||||
files = files.filter((f) => f.name !== url);
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
||||
|
||||
@ -346,48 +278,9 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full relative">
|
||||
{#if prompt.charAt(0) === '/'}
|
||||
<Prompts bind:this={promptsElement} bind:prompt bind:files />
|
||||
{:else if prompt.charAt(0) === '#'}
|
||||
<Documents
|
||||
bind:this={documentsElement}
|
||||
bind:prompt
|
||||
on:youtube={(e) => {
|
||||
console.log(e);
|
||||
uploadYoutubeTranscription(e.detail);
|
||||
}}
|
||||
on:url={(e) => {
|
||||
console.log(e);
|
||||
uploadWeb(e.detail);
|
||||
}}
|
||||
on:select={(e) => {
|
||||
console.log(e);
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: e?.detail?.type ?? 'file',
|
||||
...e.detail,
|
||||
status: 'processed'
|
||||
}
|
||||
];
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Models
|
||||
bind:this={modelsElement}
|
||||
bind:prompt
|
||||
bind:chatInputPlaceholder
|
||||
{messages}
|
||||
on:select={(e) => {
|
||||
atSelectedModel = e.detail;
|
||||
chatTextAreaElement?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if atSelectedModel !== undefined}
|
||||
<div
|
||||
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900 z-50"
|
||||
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900 z-10"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-sm dark:text-gray-500">
|
||||
<img
|
||||
@ -416,6 +309,21 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Commands
|
||||
bind:this={commandsElement}
|
||||
bind:prompt
|
||||
bind:files
|
||||
on:select={(e) => {
|
||||
const data = e.detail;
|
||||
|
||||
if (data?.type === 'model') {
|
||||
atSelectedModel = data.data;
|
||||
}
|
||||
|
||||
chatTextAreaElement?.focus();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -641,6 +549,7 @@
|
||||
}}
|
||||
on:keydown={async (e) => {
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
const commandsContainerElement = document.getElementById('commands-container');
|
||||
|
||||
// Check if Ctrl + R is pressed
|
||||
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
|
||||
@ -671,10 +580,9 @@
|
||||
editButton?.click();
|
||||
}
|
||||
|
||||
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
|
||||
if (commandsContainerElement && e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
|
||||
(promptsElement || documentsElement || modelsElement).selectUp();
|
||||
commandsElement.selectUp();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
@ -682,10 +590,9 @@
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
|
||||
if (commandsContainerElement && e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
|
||||
(promptsElement || documentsElement || modelsElement).selectDown();
|
||||
commandsElement.selectDown();
|
||||
|
||||
const commandOptionButton = [
|
||||
...document.getElementsByClassName('selected-command-option-button')
|
||||
@ -693,7 +600,7 @@
|
||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') {
|
||||
if (commandsContainerElement && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
@ -709,7 +616,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') {
|
||||
if (commandsContainerElement && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
|
||||
const commandOptionButton = [
|
||||
@ -789,7 +696,7 @@
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
try {
|
||||
const res = await navigator.mediaDevices
|
||||
let stream = await navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
.catch(function (err) {
|
||||
toast.error(
|
||||
@ -803,9 +710,12 @@
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
if (stream) {
|
||||
recording = true;
|
||||
const tracks = stream.getTracks();
|
||||
tracks.forEach((track) => track.stop());
|
||||
}
|
||||
stream = null;
|
||||
} catch {
|
||||
toast.error($i18n.t('Permission denied when accessing microphone'));
|
||||
}
|
||||
|
131
src/lib/components/chat/MessageInput/Commands.svelte
Normal file
131
src/lib/components/chat/MessageInput/Commands.svelte
Normal file
@ -0,0 +1,131 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Prompts from './Commands/Prompts.svelte';
|
||||
import Documents from './Commands/Documents.svelte';
|
||||
import Models from './Commands/Models.svelte';
|
||||
|
||||
import { removeLastWordFromString } from '$lib/utils';
|
||||
import { uploadWebToVectorDB, uploadYoutubeTranscriptionToVectorDB } from '$lib/apis/rag';
|
||||
|
||||
export let prompt = '';
|
||||
export let files = [];
|
||||
|
||||
let commandElement = null;
|
||||
|
||||
export const selectUp = () => {
|
||||
commandElement?.selectUp();
|
||||
};
|
||||
|
||||
export const selectDown = () => {
|
||||
commandElement?.selectDown();
|
||||
};
|
||||
|
||||
let command = '';
|
||||
$: command = (prompt?.trim() ?? '').split(' ')?.at(-1) ?? '';
|
||||
|
||||
const uploadWeb = async (url) => {
|
||||
console.log(url);
|
||||
|
||||
const doc = {
|
||||
type: 'doc',
|
||||
name: url,
|
||||
collection_name: '',
|
||||
status: false,
|
||||
url: url,
|
||||
error: ''
|
||||
};
|
||||
|
||||
try {
|
||||
files = [...files, doc];
|
||||
const res = await uploadWebToVectorDB(localStorage.token, '', url);
|
||||
|
||||
if (res) {
|
||||
doc.status = 'processed';
|
||||
doc.collection_name = res.collection_name;
|
||||
files = files;
|
||||
}
|
||||
} catch (e) {
|
||||
// Remove the failed doc from the files array
|
||||
files = files.filter((f) => f.name !== url);
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadYoutubeTranscription = async (url) => {
|
||||
console.log(url);
|
||||
|
||||
const doc = {
|
||||
type: 'doc',
|
||||
name: url,
|
||||
collection_name: '',
|
||||
status: false,
|
||||
url: url,
|
||||
error: ''
|
||||
};
|
||||
|
||||
try {
|
||||
files = [...files, doc];
|
||||
const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
|
||||
|
||||
if (res) {
|
||||
doc.status = 'processed';
|
||||
doc.collection_name = res.collection_name;
|
||||
files = files;
|
||||
}
|
||||
} catch (e) {
|
||||
// Remove the failed doc from the files array
|
||||
files = files.filter((f) => f.name !== url);
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if ['/', '#', '@'].includes(command?.charAt(0))}
|
||||
{#if command?.charAt(0) === '/'}
|
||||
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
|
||||
{:else if command?.charAt(0) === '#'}
|
||||
<Documents
|
||||
bind:this={commandElement}
|
||||
bind:prompt
|
||||
{command}
|
||||
on:youtube={(e) => {
|
||||
console.log(e);
|
||||
uploadYoutubeTranscription(e.detail);
|
||||
}}
|
||||
on:url={(e) => {
|
||||
console.log(e);
|
||||
uploadWeb(e.detail);
|
||||
}}
|
||||
on:select={(e) => {
|
||||
console.log(e);
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: e?.detail?.type ?? 'file',
|
||||
...e.detail,
|
||||
status: 'processed'
|
||||
}
|
||||
];
|
||||
|
||||
dispatch('select');
|
||||
}}
|
||||
/>
|
||||
{:else if command?.charAt(0) === '@'}
|
||||
<Models
|
||||
bind:this={commandElement}
|
||||
{command}
|
||||
on:select={(e) => {
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
|
||||
dispatch('select', {
|
||||
type: 'model',
|
||||
data: e.detail
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
@ -9,6 +9,7 @@
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let prompt = '';
|
||||
export let command = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let selectedIdx = 0;
|
||||
@ -43,16 +44,16 @@
|
||||
];
|
||||
|
||||
$: filteredCollections = collections
|
||||
.filter((collection) => findByName(collection, prompt))
|
||||
.filter((collection) => findByName(collection, command))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
$: filteredDocs = $documents
|
||||
.filter((doc) => findByName(doc, prompt))
|
||||
.filter((doc) => findByName(doc, command))
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
$: filteredItems = [...filteredCollections, ...filteredDocs];
|
||||
|
||||
$: if (prompt) {
|
||||
$: if (command) {
|
||||
selectedIdx = 0;
|
||||
|
||||
console.log(filteredCollections);
|
||||
@ -62,9 +63,9 @@
|
||||
name: string;
|
||||
};
|
||||
|
||||
const findByName = (obj: ObjectWithName, prompt: string) => {
|
||||
const findByName = (obj: ObjectWithName, command: string) => {
|
||||
const name = obj.name.toLowerCase();
|
||||
return name.includes(prompt.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '');
|
||||
return name.includes(command.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '');
|
||||
};
|
||||
|
||||
export const selectUp = () => {
|
||||
@ -110,7 +111,10 @@
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
|
||||
<div
|
||||
id="commands-container"
|
||||
class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
|
||||
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||
<div class=" text-lg font-semibold mt-2">#</div>
|
90
src/lib/components/chat/MessageInput/Commands/Models.svelte
Normal file
90
src/lib/components/chat/MessageInput/Commands/Models.svelte
Normal file
@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { tick, getContext } from 'svelte';
|
||||
|
||||
import { models } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let command = '';
|
||||
|
||||
let selectedIdx = 0;
|
||||
let filteredModels = [];
|
||||
|
||||
$: filteredModels = $models
|
||||
.filter((p) =>
|
||||
p.name.toLowerCase().includes(command.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '')
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
$: if (command) {
|
||||
selectedIdx = 0;
|
||||
}
|
||||
|
||||
export const selectUp = () => {
|
||||
selectedIdx = Math.max(0, selectedIdx - 1);
|
||||
};
|
||||
|
||||
export const selectDown = () => {
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredModels.length - 1);
|
||||
};
|
||||
|
||||
const confirmSelect = async (model) => {
|
||||
command = '';
|
||||
dispatch('select', model);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredModels.length > 0}
|
||||
<div
|
||||
id="commands-container"
|
||||
class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
|
||||
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||
<div class=" text-lg font-semibold mt-2">@</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredModels as model, modelIdx}
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? 'bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
confirmSelect(model);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = modelIdx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
alt={model?.name ?? model.id}
|
||||
class="rounded-full size-6 items-center mr-2"
|
||||
/>
|
||||
{model.name}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
@ -7,27 +7,30 @@
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let files;
|
||||
export let prompt = '';
|
||||
let selectedCommandIdx = 0;
|
||||
let filteredPromptCommands = [];
|
||||
|
||||
$: filteredPromptCommands = $prompts
|
||||
.filter((p) => p.command.toLowerCase().includes(prompt.toLowerCase()))
|
||||
export let prompt = '';
|
||||
export let command = '';
|
||||
|
||||
let selectedPromptIdx = 0;
|
||||
let filteredPrompts = [];
|
||||
|
||||
$: filteredPrompts = $prompts
|
||||
.filter((p) => p.command.toLowerCase().includes(command.toLowerCase()))
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
$: if (prompt) {
|
||||
selectedCommandIdx = 0;
|
||||
$: if (command) {
|
||||
selectedPromptIdx = 0;
|
||||
}
|
||||
|
||||
export const selectUp = () => {
|
||||
selectedCommandIdx = Math.max(0, selectedCommandIdx - 1);
|
||||
selectedPromptIdx = Math.max(0, selectedPromptIdx - 1);
|
||||
};
|
||||
|
||||
export const selectDown = () => {
|
||||
selectedCommandIdx = Math.min(selectedCommandIdx + 1, filteredPromptCommands.length - 1);
|
||||
selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
|
||||
};
|
||||
|
||||
const confirmCommand = async (command) => {
|
||||
const confirmPrompt = async (command) => {
|
||||
let text = command.content;
|
||||
|
||||
if (command.content.includes('{{CLIPBOARD}}')) {
|
||||
@ -79,7 +82,6 @@
|
||||
await tick();
|
||||
|
||||
const words = findWordIndices(prompt);
|
||||
|
||||
if (words.length > 0) {
|
||||
const word = words.at(0);
|
||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||
@ -87,8 +89,11 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if filteredPromptCommands.length > 0}
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
|
||||
{#if filteredPrompts.length > 0}
|
||||
<div
|
||||
id="commands-container"
|
||||
class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
|
||||
>
|
||||
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
|
||||
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||
<div class=" text-lg font-semibold mt-2">/</div>
|
||||
@ -98,26 +103,26 @@
|
||||
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredPromptCommands as command, commandIdx}
|
||||
{#each filteredPrompts as prompt, promptIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
|
||||
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
confirmCommand(command);
|
||||
confirmPrompt(prompt);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedCommandIdx = commandIdx;
|
||||
selectedPromptIdx = promptIdx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black dark:text-gray-100">
|
||||
{command.command}
|
||||
{prompt.command}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-100">
|
||||
{command.title}
|
||||
{prompt.title}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
@ -1,181 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { generatePrompt } from '$lib/apis/ollama';
|
||||
import { models } from '$lib/stores';
|
||||
import { splitStream } from '$lib/utils';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let prompt = '';
|
||||
export let user = null;
|
||||
|
||||
export let chatInputPlaceholder = '';
|
||||
export let messages = [];
|
||||
|
||||
let selectedIdx = 0;
|
||||
let filteredModels = [];
|
||||
|
||||
$: filteredModels = $models
|
||||
.filter((p) =>
|
||||
p.name.toLowerCase().includes(prompt.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '')
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
$: if (prompt) {
|
||||
selectedIdx = 0;
|
||||
}
|
||||
|
||||
export const selectUp = () => {
|
||||
selectedIdx = Math.max(0, selectedIdx - 1);
|
||||
};
|
||||
|
||||
export const selectDown = () => {
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredModels.length - 1);
|
||||
};
|
||||
|
||||
const confirmSelect = async (model) => {
|
||||
prompt = '';
|
||||
dispatch('select', model);
|
||||
};
|
||||
|
||||
const confirmSelectCollaborativeChat = async (model) => {
|
||||
// dispatch('select', model);
|
||||
prompt = '';
|
||||
user = JSON.parse(JSON.stringify(model.name));
|
||||
await tick();
|
||||
|
||||
chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name });
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
|
||||
const convoText = messages.reduce((a, message, i, arr) => {
|
||||
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
|
||||
}, '');
|
||||
|
||||
const res = await generatePrompt(localStorage.token, model.name, convoText);
|
||||
|
||||
if (res && res.ok) {
|
||||
const reader = res.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(splitStream('\n'))
|
||||
.getReader();
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
let lines = value.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line !== '') {
|
||||
console.log(line);
|
||||
let data = JSON.parse(line);
|
||||
|
||||
if ('detail' in data) {
|
||||
throw data;
|
||||
}
|
||||
|
||||
if ('id' in data) {
|
||||
console.log(data);
|
||||
} else {
|
||||
if (data.done == false) {
|
||||
if (prompt == '' && data.response == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
prompt += data.response;
|
||||
console.log(data.response);
|
||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if ('detail' in error) {
|
||||
toast.error(error.detail);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (res !== null) {
|
||||
const error = await res.json();
|
||||
console.log(error);
|
||||
if ('detail' in error) {
|
||||
toast.error(error.detail);
|
||||
} else {
|
||||
toast.error(error.error);
|
||||
}
|
||||
} else {
|
||||
toast.error(
|
||||
$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
chatInputPlaceholder = '';
|
||||
|
||||
console.log(user);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if prompt.charAt(0) === '@'}
|
||||
{#if filteredModels.length > 0}
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
|
||||
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
|
||||
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||
<div class=" text-lg font-semibold mt-2">@</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredModels as model, modelIdx}
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? 'bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
confirmSelect(model);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = modelIdx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
alt={model?.name ?? model.id}
|
||||
class="rounded-full size-6 items-center mr-2"
|
||||
/>
|
||||
{model.name}
|
||||
</div>
|
||||
|
||||
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
|
||||
{doc.title}
|
||||
</div> -->
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
@ -288,6 +288,20 @@ export const findWordIndices = (text) => {
|
||||
return matches;
|
||||
};
|
||||
|
||||
export const removeLastWordFromString = (inputString, wordString) => {
|
||||
// Split the string into an array of words
|
||||
const words = inputString.split(' ');
|
||||
|
||||
if (words.at(-1) === wordString) {
|
||||
words.pop();
|
||||
}
|
||||
|
||||
// Join the remaining words back into a string
|
||||
const resultString = words.join(' ');
|
||||
|
||||
return resultString;
|
||||
};
|
||||
|
||||
export const removeFirstHashWord = (inputString) => {
|
||||
// Split the string into an array of words
|
||||
const words = inputString.split(' ');
|
||||
|
Loading…
Reference in New Issue
Block a user