mirror of
https://git.mirrors.martin98.com/https://github.com/open-webui/open-webui
synced 2025-08-18 04:25:52 +08:00
refac: input commands
This commit is contained in:
parent
64c0157271
commit
591962d906
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type Model,
|
type Model,
|
||||||
mobile,
|
mobile,
|
||||||
@ -12,15 +13,9 @@
|
|||||||
tools,
|
tools,
|
||||||
user as _user
|
user as _user
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
import { blobToFile, findWordIndices } from '$lib/utils';
|
||||||
|
import { processDocToVectorDB } from '$lib/apis/rag';
|
||||||
import {
|
import { transcribeAudio } from '$lib/apis/audio';
|
||||||
processDocToVectorDB,
|
|
||||||
uploadDocToVectorDB,
|
|
||||||
uploadWebToVectorDB,
|
|
||||||
uploadYoutubeTranscriptionToVectorDB
|
|
||||||
} from '$lib/apis/rag';
|
|
||||||
|
|
||||||
import { uploadFile } from '$lib/apis/files';
|
import { uploadFile } from '$lib/apis/files';
|
||||||
import {
|
import {
|
||||||
SUPPORTED_FILE_TYPE,
|
SUPPORTED_FILE_TYPE,
|
||||||
@ -29,19 +24,14 @@
|
|||||||
WEBUI_API_BASE_URL
|
WEBUI_API_BASE_URL
|
||||||
} from '$lib/constants';
|
} 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 Tooltip from '../common/Tooltip.svelte';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
|
||||||
import InputMenu from './MessageInput/InputMenu.svelte';
|
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||||
import Headphone from '../icons/Headphone.svelte';
|
import Headphone from '../icons/Headphone.svelte';
|
||||||
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
|
||||||
import { transcribeAudio } from '$lib/apis/audio';
|
|
||||||
import FileItem from '../common/FileItem.svelte';
|
import FileItem from '../common/FileItem.svelte';
|
||||||
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
|
||||||
|
import Commands from './MessageInput/Commands.svelte';
|
||||||
|
import XMark from '../icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@ -60,9 +50,7 @@
|
|||||||
let chatTextAreaElement: HTMLTextAreaElement;
|
let chatTextAreaElement: HTMLTextAreaElement;
|
||||||
let filesInputElement;
|
let filesInputElement;
|
||||||
|
|
||||||
let promptsElement;
|
let commandsElement;
|
||||||
let documentsElement;
|
|
||||||
let modelsElement;
|
|
||||||
|
|
||||||
let inputFiles;
|
let inputFiles;
|
||||||
let dragged = false;
|
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(() => {
|
onMount(() => {
|
||||||
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
||||||
|
|
||||||
@ -346,48 +278,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<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}
|
{#if atSelectedModel !== undefined}
|
||||||
<div
|
<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">
|
<div class="flex items-center gap-2 text-sm dark:text-gray-500">
|
||||||
<img
|
<img
|
||||||
@ -416,6 +309,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -641,6 +549,7 @@
|
|||||||
}}
|
}}
|
||||||
on:keydown={async (e) => {
|
on:keydown={async (e) => {
|
||||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
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
|
// Check if Ctrl + R is pressed
|
||||||
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
|
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
|
||||||
@ -671,10 +580,9 @@
|
|||||||
editButton?.click();
|
editButton?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
|
if (commandsContainerElement && e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
commandsElement.selectUp();
|
||||||
(promptsElement || documentsElement || modelsElement).selectUp();
|
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
...document.getElementsByClassName('selected-command-option-button')
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
@ -682,10 +590,9 @@
|
|||||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
|
if (commandsContainerElement && e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
commandsElement.selectDown();
|
||||||
(promptsElement || documentsElement || modelsElement).selectDown();
|
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
...document.getElementsByClassName('selected-command-option-button')
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
@ -693,7 +600,7 @@
|
|||||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') {
|
if (commandsContainerElement && e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
@ -709,7 +616,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') {
|
if (commandsContainerElement && e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
@ -789,7 +696,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
try {
|
try {
|
||||||
const res = await navigator.mediaDevices
|
let stream = await navigator.mediaDevices
|
||||||
.getUserMedia({ audio: true })
|
.getUserMedia({ audio: true })
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -803,9 +710,12 @@
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res) {
|
if (stream) {
|
||||||
recording = true;
|
recording = true;
|
||||||
|
const tracks = stream.getTracks();
|
||||||
|
tracks.forEach((track) => track.stop());
|
||||||
}
|
}
|
||||||
|
stream = null;
|
||||||
} catch {
|
} catch {
|
||||||
toast.error($i18n.t('Permission denied when accessing microphone'));
|
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');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
|
export let command = '';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let selectedIdx = 0;
|
let selectedIdx = 0;
|
||||||
@ -43,16 +44,16 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
$: filteredCollections = collections
|
$: filteredCollections = collections
|
||||||
.filter((collection) => findByName(collection, prompt))
|
.filter((collection) => findByName(collection, command))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
$: filteredDocs = $documents
|
$: filteredDocs = $documents
|
||||||
.filter((doc) => findByName(doc, prompt))
|
.filter((doc) => findByName(doc, command))
|
||||||
.sort((a, b) => a.title.localeCompare(b.title));
|
.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
$: filteredItems = [...filteredCollections, ...filteredDocs];
|
$: filteredItems = [...filteredCollections, ...filteredDocs];
|
||||||
|
|
||||||
$: if (prompt) {
|
$: if (command) {
|
||||||
selectedIdx = 0;
|
selectedIdx = 0;
|
||||||
|
|
||||||
console.log(filteredCollections);
|
console.log(filteredCollections);
|
||||||
@ -62,9 +63,9 @@
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByName = (obj: ObjectWithName, prompt: string) => {
|
const findByName = (obj: ObjectWithName, command: string) => {
|
||||||
const name = obj.name.toLowerCase();
|
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 = () => {
|
export const selectUp = () => {
|
||||||
@ -110,7 +111,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
{#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="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=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||||
<div class=" text-lg font-semibold mt-2">#</div>
|
<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');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let files;
|
export let files;
|
||||||
export let prompt = '';
|
|
||||||
let selectedCommandIdx = 0;
|
|
||||||
let filteredPromptCommands = [];
|
|
||||||
|
|
||||||
$: filteredPromptCommands = $prompts
|
export let prompt = '';
|
||||||
.filter((p) => p.command.toLowerCase().includes(prompt.toLowerCase()))
|
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));
|
.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
$: if (prompt) {
|
$: if (command) {
|
||||||
selectedCommandIdx = 0;
|
selectedPromptIdx = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectUp = () => {
|
export const selectUp = () => {
|
||||||
selectedCommandIdx = Math.max(0, selectedCommandIdx - 1);
|
selectedPromptIdx = Math.max(0, selectedPromptIdx - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const selectDown = () => {
|
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;
|
let text = command.content;
|
||||||
|
|
||||||
if (command.content.includes('{{CLIPBOARD}}')) {
|
if (command.content.includes('{{CLIPBOARD}}')) {
|
||||||
@ -79,7 +82,6 @@
|
|||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
const words = findWordIndices(prompt);
|
const words = findWordIndices(prompt);
|
||||||
|
|
||||||
if (words.length > 0) {
|
if (words.length > 0) {
|
||||||
const word = words.at(0);
|
const word = words.at(0);
|
||||||
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||||
@ -87,8 +89,11 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredPromptCommands.length > 0}
|
{#if filteredPrompts.length > 0}
|
||||||
<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="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=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
|
||||||
<div class=" text-lg font-semibold mt-2">/</div>
|
<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"
|
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">
|
<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
|
<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'
|
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||||
: ''}"
|
: ''}"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
confirmCommand(command);
|
confirmPrompt(prompt);
|
||||||
}}
|
}}
|
||||||
on:mousemove={() => {
|
on:mousemove={() => {
|
||||||
selectedCommandIdx = commandIdx;
|
selectedPromptIdx = promptIdx;
|
||||||
}}
|
}}
|
||||||
on:focus={() => {}}
|
on:focus={() => {}}
|
||||||
>
|
>
|
||||||
<div class=" font-medium text-black dark:text-gray-100">
|
<div class=" font-medium text-black dark:text-gray-100">
|
||||||
{command.command}
|
{prompt.command}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" text-xs text-gray-600 dark:text-gray-100">
|
<div class=" text-xs text-gray-600 dark:text-gray-100">
|
||||||
{command.title}
|
{prompt.title}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/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;
|
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) => {
|
export const removeFirstHashWord = (inputString) => {
|
||||||
// Split the string into an array of words
|
// Split the string into an array of words
|
||||||
const words = inputString.split(' ');
|
const words = inputString.split(' ');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user