Timothy J. Baek 1 год назад
Родитель
Сommit
70029d9bed

+ 39 - 0
src/lib/apis/ollama/index.ts

@@ -167,6 +167,45 @@ export const generateTitle = async (token: string = '', model: string, prompt: s
 	return res?.response ?? 'New Chat';
 };
 
+export const generatePrompt = async (token: string = '', model: string, conversation: string) => {
+	let error = null;
+
+	if (conversation === '') {
+		conversation = '[You need to start the conversation]';
+	}
+
+	const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'text/event-stream',
+			Authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			model: model,
+			prompt: `Based on the following conversation, you are playing the role of 'USER.' Your task is to provide a thoughtful and appropriate response to the last message in the conversation, taking into account the context and tone of the discussion.
+
+			Conversation:
+			${conversation}
+
+			As USER, how would you respond to the latest message? If no previous conversation is provided, start a new conversation with a common, friendly greeting or a relevant question. If there is an existing conversation, continue it by providing a thoughtful, relevant, and engaging response.
+			Response:
+			`
+		})
+	}).catch((err) => {
+		console.log(err);
+		if ('detail' in err) {
+			error = err.detail;
+		}
+		return null;
+	});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const generateChatCompletion = async (token: string = '', body: object) => {
 	let error = null;
 

+ 38 - 10
src/lib/components/chat/MessageInput.svelte

@@ -10,6 +10,7 @@
 	import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
 	import { SUPPORTED_FILE_TYPE } from '$lib/constants';
 	import Documents from './MessageInput/Documents.svelte';
+	import Models from './MessageInput/Models.svelte';
 
 	export let submitPrompt: Function;
 	export let stopResponse: Function;
@@ -18,12 +19,17 @@
 	export let autoScroll = true;
 
 	let filesInputElement;
+
 	let promptsElement;
 	let documentsElement;
+	let modelsElement;
 
 	let inputFiles;
 	let dragged = false;
 
+	let user = null;
+	let chatInputPlaceholder = '';
+
 	export let files = [];
 
 	export let fileUploadEnabled = true;
@@ -35,6 +41,15 @@
 
 	let speechRecognition;
 
+	$: if (prompt) {
+		const chatInput = document.getElementById('chat-textarea');
+
+		if (chatInput) {
+			chatInput.style.height = '';
+			chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
+		}
+	}
+
 	const speechRecognitionHandler = () => {
 		// Check if SpeechRecognition is supported
 
@@ -79,7 +94,7 @@
 					console.log('recognition ended');
 					speechRecognitionListening = false;
 					if (prompt !== '' && $settings?.speechAutoSend === true) {
-						submitPrompt(prompt);
+						submitPrompt(prompt, user);
 					}
 				};
 
@@ -242,6 +257,14 @@
 							];
 						}}
 					/>
+				{:else if prompt.charAt(0) === '@'}
+					<Models
+						bind:this={modelsElement}
+						bind:prompt
+						bind:user
+						bind:chatInputPlaceholder
+						{messages}
+					/>
 				{:else if messages.length == 0 && suggestionPrompts.length !== 0}
 					<Suggestions {suggestionPrompts} {submitPrompt} />
 				{/if}
@@ -289,7 +312,7 @@
 				<form
 					class=" flex flex-col relative w-full rounded-xl border dark:border-gray-600 bg-white dark:bg-gray-800 dark:text-gray-100"
 					on:submit|preventDefault={() => {
-						submitPrompt(prompt);
+						submitPrompt(prompt, user);
 					}}
 				>
 					{#if files.length > 0}
@@ -431,14 +454,18 @@
 							class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled
 								? ''
 								: ' pl-4'} rounded-xl resize-none h-[48px]"
-							placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'}
+							placeholder={chatInputPlaceholder !== ''
+								? chatInputPlaceholder
+								: speechRecognitionListening
+								? 'Listening...'
+								: 'Send a message'}
 							bind:value={prompt}
 							on:keypress={(e) => {
 								if (e.keyCode == 13 && !e.shiftKey) {
 									e.preventDefault();
 								}
 								if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
-									submitPrompt(prompt);
+									submitPrompt(prompt, user);
 								}
 							}}
 							on:keydown={async (e) => {
@@ -473,10 +500,10 @@
 									editButton?.click();
 								}
 
-								if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
+								if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
 									e.preventDefault();
 
-									(promptsElement || documentsElement).selectUp();
+									(promptsElement || documentsElement || modelsElement).selectUp();
 
 									const commandOptionButton = [
 										...document.getElementsByClassName('selected-command-option-button')
@@ -484,10 +511,10 @@
 									commandOptionButton.scrollIntoView({ block: 'center' });
 								}
 
-								if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
+								if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
 									e.preventDefault();
 
-									(promptsElement || documentsElement).selectDown();
+									(promptsElement || documentsElement || modelsElement).selectDown();
 
 									const commandOptionButton = [
 										...document.getElementsByClassName('selected-command-option-button')
@@ -495,7 +522,7 @@
 									commandOptionButton.scrollIntoView({ block: 'center' });
 								}
 
-								if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Enter') {
+								if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') {
 									e.preventDefault();
 
 									const commandOptionButton = [
@@ -505,7 +532,7 @@
 									commandOptionButton?.click();
 								}
 
-								if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Tab') {
+								if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') {
 									e.preventDefault();
 
 									const commandOptionButton = [
@@ -536,6 +563,7 @@
 							on:input={(e) => {
 								e.target.style.height = '';
 								e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
+								user = null;
 							}}
 							on:paste={(e) => {
 								const clipboardData = e.clipboardData || window.clipboardData;

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

@@ -0,0 +1,153 @@
+<script lang="ts">
+	import { generatePrompt } from '$lib/apis/ollama';
+	import { models } from '$lib/stores';
+	import { splitStream } from '$lib/utils';
+	import { tick } from 'svelte';
+	import toast from 'svelte-french-toast';
+
+	export let prompt = '';
+	export let user = null;
+
+	export let chatInputPlaceholder = '';
+	export let messages = [];
+
+	let selectedIdx = 0;
+	let filteredModels = [];
+
+	$: filteredModels = $models
+		.filter((p) => p.name !== 'hr' && p.name.includes(prompt.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) => {
+		// dispatch('select', model);
+		prompt = '';
+		user = JSON.parse(JSON.stringify(model.name));
+		await tick();
+
+		chatInputPlaceholder = `'${model.name}' is thinking...`;
+
+		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 (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(`Uh-oh! There was an issue connecting to Ollama.`);
+			}
+		}
+
+		chatInputPlaceholder = '';
+
+		console.log(user);
+	};
+</script>
+
+{#if filteredModels.length > 0}
+	<div class="md:px-2 mb-3 text-left w-full">
+		<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
+			<div class=" bg-gray-100 dark:bg-gray-700 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">
+				<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
+					{#each filteredModels as model, modelIdx}
+						<button
+							class=" px-3 py-1.5 rounded-lg w-full text-left {modelIdx === selectedIdx
+								? ' bg-gray-100 selected-command-option-button'
+								: ''}"
+							type="button"
+							on:click={() => {
+								confirmSelect(model);
+							}}
+							on:mousemove={() => {
+								selectedIdx = modelIdx;
+							}}
+							on:focus={() => {}}
+						>
+							<div class=" font-medium text-black line-clamp-1">
+								{model.name}
+							</div>
+
+							<!-- <div class=" text-xs text-gray-600 line-clamp-1">
+								{doc.title}
+							</div> -->
+						</button>
+					{/each}
+				</div>
+			</div>
+		</div>
+	</div>
+{/if}

+ 1 - 1
src/lib/components/chat/MessageInput/Suggestions.svelte

@@ -9,7 +9,7 @@
 			<button
 				class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-white hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg transition group"
 				on:click={() => {
-					submitPrompt(prompt.content);
+					submitPrompt(prompt.content, '');
 				}}
 			>
 				<div class="flex flex-col text-left self-center">

+ 18 - 2
src/lib/components/chat/Messages/UserMessage.svelte

@@ -2,6 +2,8 @@
 	import { tick } from 'svelte';
 	import Name from './Name.svelte';
 	import ProfileImage from './ProfileImage.svelte';
+	import { modelfiles } from '$lib/stores';
+	import { stringify } from 'postcss';
 
 	export let user;
 	export let message;
@@ -42,11 +44,25 @@
 </script>
 
 <div class=" flex w-full">
-	<ProfileImage src={user?.profile_image_url ?? '/user.png'} />
+	<ProfileImage
+		src={message.user
+			? $modelfiles.find((modelfile) => modelfile.tagName === message.user)?.imageUrl ?? '/user.png'
+			: user?.profile_image_url ?? '/user.png'}
+	/>
 
 	<div class="w-full overflow-hidden">
 		<div class="user-message">
-			<Name>You</Name>
+			<Name>
+				{#if message.user}
+					{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
+						{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
+					{:else}
+						You <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
+					{/if}
+				{:else}
+					You
+				{/if}
+			</Name>
 		</div>
 
 		<div

+ 2 - 1
src/routes/(app)/+page.svelte

@@ -116,7 +116,7 @@
 	// Ollama functions
 	//////////////////////////
 
-	const submitPrompt = async (userPrompt) => {
+	const submitPrompt = async (userPrompt, _user = null) => {
 		console.log('submitPrompt', $chatId);
 
 		if (selectedModels.includes('')) {
@@ -143,6 +143,7 @@
 				parentId: messages.length !== 0 ? messages.at(-1).id : null,
 				childrenIds: [],
 				role: 'user',
+				user: _user ?? undefined,
 				content: userPrompt,
 				files: files.length > 0 ? files : undefined
 			};

+ 10 - 1
src/routes/(app)/c/[id]/+page.svelte

@@ -135,7 +135,8 @@
 	// Ollama functions
 	//////////////////////////
 
-	const submitPrompt = async (userPrompt) => {
+	const submitPrompt = async (userPrompt, user) => {
+		console.log(userPrompt, user);
 		console.log('submitPrompt', $chatId);
 
 		if (selectedModels.includes('')) {
@@ -143,6 +144,14 @@
 		} else if (messages.length != 0 && messages.at(-1).done != true) {
 			// Response not done
 			console.log('wait');
+		} else if (
+			files.length > 0 &&
+			files.filter((file) => file.upload_status === false).length > 0
+		) {
+			// Upload not done
+			toast.error(
+				`Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.`
+			);
 		} else {
 			// Reset chat message textarea height
 			document.getElementById('chat-textarea').style.height = '';