فهرست منبع

refac: input commands

Timothy J. Baek 8 ماه پیش
والد
کامیت
591962d906

+ 35 - 125
src/lib/components/chat/MessageInput.svelte

@@ -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 {
-		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 { 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 documentsElement;
-	let modelsElement;
+	let commandsElement;
 
 
 	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();
-
-											(promptsElement || documentsElement || modelsElement).selectUp();
+											commandsElement.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();
-
-											(promptsElement || documentsElement || modelsElement).selectDown();
+											commandsElement.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 - 0
src/lib/components/chat/MessageInput/Commands.svelte

@@ -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}

+ 10 - 6
src/lib/components/chat/MessageInput/Documents.svelte → src/lib/components/chat/MessageInput/Commands/Documents.svelte

@@ -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 - 0
src/lib/components/chat/MessageInput/Commands/Models.svelte

@@ -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}

+ 23 - 18
src/lib/components/chat/MessageInput/PromptCommands.svelte → src/lib/components/chat/MessageInput/Commands/Prompts.svelte

@@ -7,27 +7,30 @@
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	export let files;
 	export let files;
+
 	export let prompt = '';
 	export let prompt = '';
-	let selectedCommandIdx = 0;
-	let filteredPromptCommands = [];
+	export let command = '';
+
+	let selectedPromptIdx = 0;
+	let filteredPrompts = [];
 
 
-	$: filteredPromptCommands = $prompts
-		.filter((p) => p.command.toLowerCase().includes(prompt.toLowerCase()))
+	$: 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) {
-		selectedCommandIdx = 0;
+	$: if (command) {
+		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}
-	<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="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}

+ 0 - 181
src/lib/components/chat/MessageInput/Models.svelte

@@ -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}

+ 14 - 0
src/lib/utils/index.ts

@@ -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(' ');