forked from open-webui/open-webui
feat: @model group convo
This commit is contained in:
parent
358f79f533
commit
70029d9bed
7 changed files with 261 additions and 15 deletions
|
@ -167,6 +167,45 @@ export const generateTitle = async (token: string = '', model: string, prompt: s
|
||||||
return res?.response ?? 'New Chat';
|
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) => {
|
export const generateChatCompletion = async (token: string = '', body: object) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
|
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
|
||||||
import { SUPPORTED_FILE_TYPE } from '$lib/constants';
|
import { SUPPORTED_FILE_TYPE } from '$lib/constants';
|
||||||
import Documents from './MessageInput/Documents.svelte';
|
import Documents from './MessageInput/Documents.svelte';
|
||||||
|
import Models from './MessageInput/Models.svelte';
|
||||||
|
|
||||||
export let submitPrompt: Function;
|
export let submitPrompt: Function;
|
||||||
export let stopResponse: Function;
|
export let stopResponse: Function;
|
||||||
|
@ -18,12 +19,17 @@
|
||||||
export let autoScroll = true;
|
export let autoScroll = true;
|
||||||
|
|
||||||
let filesInputElement;
|
let filesInputElement;
|
||||||
|
|
||||||
let promptsElement;
|
let promptsElement;
|
||||||
let documentsElement;
|
let documentsElement;
|
||||||
|
let modelsElement;
|
||||||
|
|
||||||
let inputFiles;
|
let inputFiles;
|
||||||
let dragged = false;
|
let dragged = false;
|
||||||
|
|
||||||
|
let user = null;
|
||||||
|
let chatInputPlaceholder = '';
|
||||||
|
|
||||||
export let files = [];
|
export let files = [];
|
||||||
|
|
||||||
export let fileUploadEnabled = true;
|
export let fileUploadEnabled = true;
|
||||||
|
@ -35,6 +41,15 @@
|
||||||
|
|
||||||
let speechRecognition;
|
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 = () => {
|
const speechRecognitionHandler = () => {
|
||||||
// Check if SpeechRecognition is supported
|
// Check if SpeechRecognition is supported
|
||||||
|
|
||||||
|
@ -79,7 +94,7 @@
|
||||||
console.log('recognition ended');
|
console.log('recognition ended');
|
||||||
speechRecognitionListening = false;
|
speechRecognitionListening = false;
|
||||||
if (prompt !== '' && $settings?.speechAutoSend === true) {
|
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}
|
{:else if messages.length == 0 && suggestionPrompts.length !== 0}
|
||||||
<Suggestions {suggestionPrompts} {submitPrompt} />
|
<Suggestions {suggestionPrompts} {submitPrompt} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -289,7 +312,7 @@
|
||||||
<form
|
<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"
|
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={() => {
|
on:submit|preventDefault={() => {
|
||||||
submitPrompt(prompt);
|
submitPrompt(prompt, user);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if files.length > 0}
|
{#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
|
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]"
|
: ' 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}
|
bind:value={prompt}
|
||||||
on:keypress={(e) => {
|
on:keypress={(e) => {
|
||||||
if (e.keyCode == 13 && !e.shiftKey) {
|
if (e.keyCode == 13 && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
|
if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
|
||||||
submitPrompt(prompt);
|
submitPrompt(prompt, user);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:keydown={async (e) => {
|
on:keydown={async (e) => {
|
||||||
|
@ -473,10 +500,10 @@
|
||||||
editButton?.click();
|
editButton?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
|
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
(promptsElement || documentsElement).selectUp();
|
(promptsElement || documentsElement || modelsElement).selectUp();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
...document.getElementsByClassName('selected-command-option-button')
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
|
@ -484,10 +511,10 @@
|
||||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
|
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
(promptsElement || documentsElement).selectDown();
|
(promptsElement || documentsElement || modelsElement).selectDown();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
...document.getElementsByClassName('selected-command-option-button')
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
|
@ -495,7 +522,7 @@
|
||||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Enter') {
|
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
|
@ -505,7 +532,7 @@
|
||||||
commandOptionButton?.click();
|
commandOptionButton?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/', '#'].includes(prompt.charAt(0)) && e.key === 'Tab') {
|
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
|
@ -536,6 +563,7 @@
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
e.target.style.height = '';
|
e.target.style.height = '';
|
||||||
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
|
||||||
|
user = null;
|
||||||
}}
|
}}
|
||||||
on:paste={(e) => {
|
on:paste={(e) => {
|
||||||
const clipboardData = e.clipboardData || window.clipboardData;
|
const clipboardData = e.clipboardData || window.clipboardData;
|
||||||
|
|
153
src/lib/components/chat/MessageInput/Models.svelte
Normal file
153
src/lib/components/chat/MessageInput/Models.svelte
Normal file
|
@ -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}
|
|
@ -9,7 +9,7 @@
|
||||||
<button
|
<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"
|
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={() => {
|
on:click={() => {
|
||||||
submitPrompt(prompt.content);
|
submitPrompt(prompt.content, '');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col text-left self-center">
|
<div class="flex flex-col text-left self-center">
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import Name from './Name.svelte';
|
import Name from './Name.svelte';
|
||||||
import ProfileImage from './ProfileImage.svelte';
|
import ProfileImage from './ProfileImage.svelte';
|
||||||
|
import { modelfiles } from '$lib/stores';
|
||||||
|
import { stringify } from 'postcss';
|
||||||
|
|
||||||
export let user;
|
export let user;
|
||||||
export let message;
|
export let message;
|
||||||
|
@ -42,11 +44,25 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class=" flex w-full">
|
<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="w-full overflow-hidden">
|
||||||
<div class="user-message">
|
<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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -116,7 +116,7 @@
|
||||||
// Ollama functions
|
// Ollama functions
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
|
|
||||||
const submitPrompt = async (userPrompt) => {
|
const submitPrompt = async (userPrompt, _user = null) => {
|
||||||
console.log('submitPrompt', $chatId);
|
console.log('submitPrompt', $chatId);
|
||||||
|
|
||||||
if (selectedModels.includes('')) {
|
if (selectedModels.includes('')) {
|
||||||
|
@ -143,6 +143,7 @@
|
||||||
parentId: messages.length !== 0 ? messages.at(-1).id : null,
|
parentId: messages.length !== 0 ? messages.at(-1).id : null,
|
||||||
childrenIds: [],
|
childrenIds: [],
|
||||||
role: 'user',
|
role: 'user',
|
||||||
|
user: _user ?? undefined,
|
||||||
content: userPrompt,
|
content: userPrompt,
|
||||||
files: files.length > 0 ? files : undefined
|
files: files.length > 0 ? files : undefined
|
||||||
};
|
};
|
||||||
|
|
|
@ -135,7 +135,8 @@
|
||||||
// Ollama functions
|
// Ollama functions
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
|
|
||||||
const submitPrompt = async (userPrompt) => {
|
const submitPrompt = async (userPrompt, user) => {
|
||||||
|
console.log(userPrompt, user);
|
||||||
console.log('submitPrompt', $chatId);
|
console.log('submitPrompt', $chatId);
|
||||||
|
|
||||||
if (selectedModels.includes('')) {
|
if (selectedModels.includes('')) {
|
||||||
|
@ -143,6 +144,14 @@
|
||||||
} else if (messages.length != 0 && messages.at(-1).done != true) {
|
} else if (messages.length != 0 && messages.at(-1).done != true) {
|
||||||
// Response not done
|
// Response not done
|
||||||
console.log('wait');
|
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 {
|
} else {
|
||||||
// Reset chat message textarea height
|
// Reset chat message textarea height
|
||||||
document.getElementById('chat-textarea').style.height = '';
|
document.getElementById('chat-textarea').style.height = '';
|
||||||
|
|
Loading…
Reference in a new issue