| | <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) => { |
| | |
| | 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> |
| |
|
| | |
| | |
| | |
| | </button> |
| | {/each} |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | {/if} |
| | {/if} |
| |
|