forked from open-webui/open-webui
		
	Merge branch 'main' of https://github.com/anuraagdjain/ollama-webui into feat/parallel-model-downloads
This commit is contained in:
		
						commit
						17a6ca505b
					
				
					 68 changed files with 4028 additions and 2158 deletions
				
			
		| 
						 | 
				
			
			@ -298,7 +298,7 @@
 | 
			
		|||
							id="chat-textarea"
 | 
			
		||||
							class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled
 | 
			
		||||
								? ''
 | 
			
		||||
								: ' pl-4'} rounded-xl resize-none"
 | 
			
		||||
								: ' pl-4'} rounded-xl resize-none h-[48px]"
 | 
			
		||||
							placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'}
 | 
			
		||||
							bind:value={prompt}
 | 
			
		||||
							on:keypress={(e) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,158 +1,13 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { prompts } from '$lib/stores';
 | 
			
		||||
	import { findWordIndices } from '$lib/utils';
 | 
			
		||||
	import { tick } from 'svelte';
 | 
			
		||||
 | 
			
		||||
	export let prompt = '';
 | 
			
		||||
 | 
			
		||||
	let selectedCommandIdx = 0;
 | 
			
		||||
 | 
			
		||||
	let promptCommands = [
 | 
			
		||||
		{
 | 
			
		||||
			command: '/article',
 | 
			
		||||
			title: 'Article Generator',
 | 
			
		||||
			content: `Write an article about [topic]
 | 
			
		||||
 | 
			
		||||
include relevant statistics (add the links of the sources you use) and consider diverse perspectives. Write it in a [X_tone] and mention the source links in the end.`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/backlink',
 | 
			
		||||
 | 
			
		||||
			title: 'Backlink Outreach Email',
 | 
			
		||||
			content: `Write a link-exchange outreach email on behalf of [your name] from [your_company] to ask for a backlink from their [website_url] to [your website url].`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/faq',
 | 
			
		||||
 | 
			
		||||
			title: 'FAQ Generator',
 | 
			
		||||
			content: `Create a list of [10] frequently asked questions about [keyword] and provide answers for each one of them considering the SERP and rich result guidelines.`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/headline',
 | 
			
		||||
 | 
			
		||||
			title: 'Headline Generator',
 | 
			
		||||
			content: `Generate 10 attention-grabbing headlines for an article about [your topic]`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/product',
 | 
			
		||||
 | 
			
		||||
			title: 'Product Description',
 | 
			
		||||
			content: `Craft an irresistible product description that highlights the benefits of [your product]`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/seo',
 | 
			
		||||
 | 
			
		||||
			title: 'SEO Content Brief',
 | 
			
		||||
			content: `Create a SEO content brief for [keyword].`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/seo-ideas',
 | 
			
		||||
 | 
			
		||||
			title: 'SEO Keyword Ideas',
 | 
			
		||||
			content: `Generate a list of 20 keyword ideas on [topic].
 | 
			
		||||
 | 
			
		||||
Cluster this list of keywords according to funnel stages whether they are top of the funnel, middle of the funnel or bottom of the funnel keywords.`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/summary',
 | 
			
		||||
 | 
			
		||||
			title: 'Short Summary',
 | 
			
		||||
			content: `Write a summary in 50 words that summarizes [topic or keyword].`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/email-subject',
 | 
			
		||||
 | 
			
		||||
			title: 'Email Subject Line',
 | 
			
		||||
			content: `Develop [5] subject lines for a cold email offering your [product or service] to a potential client.`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/facebook-ads',
 | 
			
		||||
 | 
			
		||||
			title: 'Facebook Ads',
 | 
			
		||||
			content: `Create 3 variations of effective ad copy to promote [product] for [audience].
 | 
			
		||||
 | 
			
		||||
Make sure they are [persuasive/playful/emotional] and mention these benefits:
 | 
			
		||||
 | 
			
		||||
[Benefit 1]
 | 
			
		||||
 | 
			
		||||
[Benefit 2]
 | 
			
		||||
 | 
			
		||||
[Benefit 3]
 | 
			
		||||
 | 
			
		||||
Finish with a call to action saying [CTA].
 | 
			
		||||
 | 
			
		||||
Add 3 emojis to it.`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/google-ads',
 | 
			
		||||
 | 
			
		||||
			title: 'Google Ads',
 | 
			
		||||
			content: `Create 10 google ads (a headline and a description) for [product description] targeting the keyword [keyword].
 | 
			
		||||
 | 
			
		||||
The headline of the ad needs to be under 30 characters. The description needs to be under 90 characters. Format the output as a table.`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/insta-caption',
 | 
			
		||||
 | 
			
		||||
			title: 'Instagram Caption',
 | 
			
		||||
			content: `Write 5 variations of Instagram captions for [product].
 | 
			
		||||
 | 
			
		||||
Use friendly, human-like language that appeals to [target audience].
 | 
			
		||||
 | 
			
		||||
Emphasize the unique qualities of [product],
 | 
			
		||||
 | 
			
		||||
use ample emojis, and don't sound too promotional.`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/linkedin-post',
 | 
			
		||||
 | 
			
		||||
			title: 'LinkedIn Post',
 | 
			
		||||
			content: `Create a narrative Linkedin post using immersive writing about [topic].
 | 
			
		||||
 | 
			
		||||
Details:
 | 
			
		||||
 | 
			
		||||
[give details in bullet point format]
 | 
			
		||||
 | 
			
		||||
Use a mix of short and long sentences. Make it punchy and dramatic.`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/youtube-desc',
 | 
			
		||||
 | 
			
		||||
			title: 'YouTube Video',
 | 
			
		||||
			content: `Write a 100-word YouTube video description that compels [audience]
 | 
			
		||||
 | 
			
		||||
to watch a video on [topic]
 | 
			
		||||
 | 
			
		||||
and mentions the following keywords
 | 
			
		||||
 | 
			
		||||
[keyword 1]
 | 
			
		||||
 | 
			
		||||
[keyword 2]
 | 
			
		||||
 | 
			
		||||
[keyword 3].`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/seo-meta',
 | 
			
		||||
 | 
			
		||||
			title: 'SEO Meta',
 | 
			
		||||
			content: `Suggest a meta description for the content above, make it user-friendly and with a call to action, include the keyword [keyword].`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/eli5',
 | 
			
		||||
 | 
			
		||||
			title: 'ELI5',
 | 
			
		||||
			content: `You are an expert teacher with the ability to explain complex topics in simpler terms. Explain the concept of [topic] in simple terms, so that my [grade level/subject] class can understand [this concept/specific example]?`
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			command: '/emoji-translate',
 | 
			
		||||
 | 
			
		||||
			title: 'Emoji Translation',
 | 
			
		||||
			content: `You are an emoji expert. Using only emojis, translate the following text to emojis. [insert numbered sentences].`
 | 
			
		||||
		}
 | 
			
		||||
	];
 | 
			
		||||
 | 
			
		||||
	let filteredPromptCommands = [];
 | 
			
		||||
 | 
			
		||||
	$: filteredPromptCommands = promptCommands
 | 
			
		||||
	$: filteredPromptCommands = $prompts
 | 
			
		||||
		.filter((p) => p.command.includes(prompt))
 | 
			
		||||
		.sort((a, b) => a.title.localeCompare(b.title));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -195,32 +50,61 @@ and mentions the following keywords
 | 
			
		|||
	<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-medium mt-2">/</div>
 | 
			
		||||
				<div class=" text-lg font-semibold mt-2">/</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class=" max-h-60 overflow-y-auto bg-white w-full p-2 rounded-r-lg space-y-0.5">
 | 
			
		||||
				{#each filteredPromptCommands as command, commandIdx}
 | 
			
		||||
					<button
 | 
			
		||||
						class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx
 | 
			
		||||
							? ' bg-gray-100 selected-command-option-button'
 | 
			
		||||
							: ''}"
 | 
			
		||||
						type="button"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							confirmCommand(command);
 | 
			
		||||
						}}
 | 
			
		||||
						on:mousemove={() => {
 | 
			
		||||
							selectedCommandIdx = commandIdx;
 | 
			
		||||
						}}
 | 
			
		||||
						on:focus={() => {}}
 | 
			
		||||
					>
 | 
			
		||||
						<div class=" font-medium text-black">
 | 
			
		||||
							{command.command}
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div class=" text-xs text-gray-600">
 | 
			
		||||
							{command.title}
 | 
			
		||||
						</div>
 | 
			
		||||
					</button>
 | 
			
		||||
				{/each}
 | 
			
		||||
			<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 filteredPromptCommands as command, commandIdx}
 | 
			
		||||
						<button
 | 
			
		||||
							class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx
 | 
			
		||||
								? ' bg-gray-100 selected-command-option-button'
 | 
			
		||||
								: ''}"
 | 
			
		||||
							type="button"
 | 
			
		||||
							on:click={() => {
 | 
			
		||||
								confirmCommand(command);
 | 
			
		||||
							}}
 | 
			
		||||
							on:mousemove={() => {
 | 
			
		||||
								selectedCommandIdx = commandIdx;
 | 
			
		||||
							}}
 | 
			
		||||
							on:focus={() => {}}
 | 
			
		||||
						>
 | 
			
		||||
							<div class=" font-medium text-black">
 | 
			
		||||
								{command.command}
 | 
			
		||||
							</div>
 | 
			
		||||
 | 
			
		||||
							<div class=" text-xs text-gray-600">
 | 
			
		||||
								{command.title}
 | 
			
		||||
							</div>
 | 
			
		||||
						</button>
 | 
			
		||||
					{/each}
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div
 | 
			
		||||
					class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-lg flex items-center space-x-1"
 | 
			
		||||
				>
 | 
			
		||||
					<div>
 | 
			
		||||
						<svg
 | 
			
		||||
							xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
							fill="none"
 | 
			
		||||
							viewBox="0 0 24 24"
 | 
			
		||||
							stroke-width="1.5"
 | 
			
		||||
							stroke="currentColor"
 | 
			
		||||
							class="w-3 h-3"
 | 
			
		||||
						>
 | 
			
		||||
							<path
 | 
			
		||||
								stroke-linecap="round"
 | 
			
		||||
								stroke-linejoin="round"
 | 
			
		||||
								d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
 | 
			
		||||
							/>
 | 
			
		||||
						</svg>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="line-clamp-1">
 | 
			
		||||
						Tip: Update multiple variable slots consecutively by pressing the tab key in the chat
 | 
			
		||||
						input after each replacement.
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
	{#each suggestionPrompts as prompt, promptIdx}
 | 
			
		||||
		<div class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px]">
 | 
			
		||||
			<button
 | 
			
		||||
				class=" flex-1 flex justify-between w-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={() => {
 | 
			
		||||
					submitPrompt(prompt.content);
 | 
			
		||||
				}}
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,9 @@
 | 
			
		|||
						<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
 | 
			
		||||
						<div class="text-sm text-gray-500">{prompt.title[1]}</div>
 | 
			
		||||
					{:else}
 | 
			
		||||
						<div class=" self-center text-sm font-medium dark:text-gray-300">{prompt.content}</div>
 | 
			
		||||
						<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
 | 
			
		||||
							{prompt.content}
 | 
			
		||||
						</div>
 | 
			
		||||
					{/if}
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,7 @@
 | 
			
		|||
					>
 | 
			
		||||
						{#if model in modelfiles}
 | 
			
		||||
							<img
 | 
			
		||||
								src={modelfiles[model]?.imageUrl}
 | 
			
		||||
								src={modelfiles[model]?.imageUrl ?? '/ollama-dark.png'}
 | 
			
		||||
								alt="modelfile"
 | 
			
		||||
								class=" w-20 mb-2 rounded-full {models.length > 1
 | 
			
		||||
									? ' border-[5px] border-white dark:border-gray-800'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,13 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { models, showSettings, settings } from '$lib/stores';
 | 
			
		||||
	import { setDefaultModels } from '$lib/apis/configs';
 | 
			
		||||
	import { models, showSettings, settings, user } from '$lib/stores';
 | 
			
		||||
	import { onMount, tick } from 'svelte';
 | 
			
		||||
	import toast from 'svelte-french-toast';
 | 
			
		||||
 | 
			
		||||
	export let selectedModels = [''];
 | 
			
		||||
	export let disabled = false;
 | 
			
		||||
 | 
			
		||||
	const saveDefaultModel = () => {
 | 
			
		||||
	const saveDefaultModel = async () => {
 | 
			
		||||
		const hasEmptyModel = selectedModels.filter((it) => it === '');
 | 
			
		||||
		if (hasEmptyModel.length) {
 | 
			
		||||
			toast.error('Choose a model before saving...');
 | 
			
		||||
| 
						 | 
				
			
			@ -13,8 +15,19 @@
 | 
			
		|||
		}
 | 
			
		||||
		settings.set({ ...$settings, models: selectedModels });
 | 
			
		||||
		localStorage.setItem('settings', JSON.stringify($settings));
 | 
			
		||||
 | 
			
		||||
		if ($user.role === 'admin') {
 | 
			
		||||
			console.log('setting default models globally');
 | 
			
		||||
			await setDefaultModels(localStorage.token, selectedModels.join(','));
 | 
			
		||||
		}
 | 
			
		||||
		toast.success('Default model updated');
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	$: if (selectedModels.length > 0 && $models.length > 0) {
 | 
			
		||||
		selectedModels = selectedModels.map((model) =>
 | 
			
		||||
			$models.map((m) => m.name).includes(model) ? model : ''
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col my-2">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,19 +8,30 @@
 | 
			
		|||
	import { splitStream, getGravatarURL } from '$lib/utils';
 | 
			
		||||
	import queue from 'async/queue';
 | 
			
		||||
 | 
			
		||||
	import { getOllamaVersion } from '$lib/apis/ollama';
 | 
			
		||||
	import { createNewChat, deleteAllChats, getAllChats, getChatList } from '$lib/apis/chats';
 | 
			
		||||
	import {
 | 
			
		||||
		WEB_UI_VERSION,
 | 
			
		||||
		OLLAMA_API_BASE_URL,
 | 
			
		||||
		WEBUI_API_BASE_URL,
 | 
			
		||||
		WEBUI_BASE_URL
 | 
			
		||||
	} from '$lib/constants';
 | 
			
		||||
		getOllamaVersion,
 | 
			
		||||
		getOllamaModels,
 | 
			
		||||
		getOllamaAPIUrl,
 | 
			
		||||
		updateOllamaAPIUrl,
 | 
			
		||||
		pullModel,
 | 
			
		||||
		createModel,
 | 
			
		||||
		deleteModel
 | 
			
		||||
	} from '$lib/apis/ollama';
 | 
			
		||||
	import { createNewChat, deleteAllChats, getAllChats, getChatList } from '$lib/apis/chats';
 | 
			
		||||
	import { WEB_UI_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
 | 
			
		||||
 | 
			
		||||
	import Advanced from './Settings/Advanced.svelte';
 | 
			
		||||
	import Modal from '../common/Modal.svelte';
 | 
			
		||||
	import { updateUserPassword } from '$lib/apis/auths';
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
	import Page from '../../../routes/(app)/+page.svelte';
 | 
			
		||||
	import {
 | 
			
		||||
		getOpenAIKey,
 | 
			
		||||
		getOpenAIModels,
 | 
			
		||||
		getOpenAIUrl,
 | 
			
		||||
		updateOpenAIKey,
 | 
			
		||||
		updateOpenAIUrl
 | 
			
		||||
	} from '$lib/apis/openai';
 | 
			
		||||
 | 
			
		||||
	export let show = false;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +45,7 @@
 | 
			
		|||
	let selectedTab = 'general';
 | 
			
		||||
 | 
			
		||||
	// General
 | 
			
		||||
	let API_BASE_URL = OLLAMA_API_BASE_URL;
 | 
			
		||||
	let API_BASE_URL = '';
 | 
			
		||||
	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
 | 
			
		||||
	let theme = 'dark';
 | 
			
		||||
	let notificationEnabled = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -77,17 +88,21 @@
 | 
			
		|||
 | 
			
		||||
	let deleteModelTag = '';
 | 
			
		||||
 | 
			
		||||
	// External
 | 
			
		||||
 | 
			
		||||
	let OPENAI_API_KEY = '';
 | 
			
		||||
	let OPENAI_API_BASE_URL = '';
 | 
			
		||||
 | 
			
		||||
	// Addons
 | 
			
		||||
	let titleAutoGenerate = true;
 | 
			
		||||
	let speechAutoSend = false;
 | 
			
		||||
	let responseAutoCopy = false;
 | 
			
		||||
 | 
			
		||||
	let gravatarEmail = '';
 | 
			
		||||
	let OPENAI_API_KEY = '';
 | 
			
		||||
	let OPENAI_API_BASE_URL = '';
 | 
			
		||||
	let titleAutoGenerateModel = '';
 | 
			
		||||
 | 
			
		||||
	// Chats
 | 
			
		||||
 | 
			
		||||
	let saveChatHistory = true;
 | 
			
		||||
	let importFiles;
 | 
			
		||||
	let showDeleteConfirm = false;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -139,22 +154,23 @@
 | 
			
		|||
	// About
 | 
			
		||||
	let ollamaVersion = '';
 | 
			
		||||
 | 
			
		||||
	const checkOllamaConnection = async () => {
 | 
			
		||||
		if (API_BASE_URL === '') {
 | 
			
		||||
			API_BASE_URL = OLLAMA_API_BASE_URL;
 | 
			
		||||
		}
 | 
			
		||||
		const _models = await getModels(API_BASE_URL, 'ollama');
 | 
			
		||||
	const updateOllamaAPIUrlHandler = async () => {
 | 
			
		||||
		API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
 | 
			
		||||
		const _models = await getModels('ollama');
 | 
			
		||||
 | 
			
		||||
		if (_models.length > 0) {
 | 
			
		||||
			toast.success('Server connection verified');
 | 
			
		||||
			await models.set(_models);
 | 
			
		||||
 | 
			
		||||
			saveSettings({
 | 
			
		||||
				API_BASE_URL: API_BASE_URL
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const updateOpenAIHandler = async () => {
 | 
			
		||||
		OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
 | 
			
		||||
		OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
 | 
			
		||||
 | 
			
		||||
		await models.set(await getModels());
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const toggleTheme = async () => {
 | 
			
		||||
		if (theme === 'dark') {
 | 
			
		||||
			theme = 'light';
 | 
			
		||||
| 
						 | 
				
			
			@ -223,60 +239,44 @@
 | 
			
		|||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const toggleAuthHeader = async () => {
 | 
			
		||||
		authEnabled = !authEnabled;
 | 
			
		||||
	const toggleSaveChatHistory = async () => {
 | 
			
		||||
		saveChatHistory = !saveChatHistory;
 | 
			
		||||
		console.log(saveChatHistory);
 | 
			
		||||
 | 
			
		||||
		if (saveChatHistory === false) {
 | 
			
		||||
			await goto('/');
 | 
			
		||||
		}
 | 
			
		||||
		saveSettings({ saveChatHistory: saveChatHistory });
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const pullModelHandlerProcessor = async (opts:{modelName:string, callback: Function}) => {
 | 
			
		||||
		console.log('Pull model name', opts.modelName);
 | 
			
		||||
		
 | 
			
		||||
		const res = await fetch(`${API_BASE_URL}/pull`, {
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'text/event-stream',
 | 
			
		||||
				...($settings.authHeader && { Authorization: $settings.authHeader }),
 | 
			
		||||
				...($user && { Authorization: `Bearer ${localStorage.token}` })
 | 
			
		||||
			},
 | 
			
		||||
			body: JSON.stringify({
 | 
			
		||||
				name: opts.modelName
 | 
			
		||||
			})
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const reader = res.body
 | 
			
		||||
			.pipeThrough(new TextDecoderStream())
 | 
			
		||||
			.pipeThrough(splitStream('\n'))
 | 
			
		||||
			.getReader();
 | 
			
		||||
		try {
 | 
			
		||||
			const res = await pullModel(localStorage.token, opts.modelName);
 | 
			
		||||
	
 | 
			
		||||
			const reader = res.body
 | 
			
		||||
				.pipeThrough(new TextDecoderStream())
 | 
			
		||||
				.pipeThrough(splitStream('\n'))
 | 
			
		||||
				.getReader();
 | 
			
		||||
 | 
			
		||||
		while (true) {
 | 
			
		||||
			const { value, done } = await reader.read();
 | 
			
		||||
			if (done) break;
 | 
			
		||||
			while (true) {
 | 
			
		||||
				try {
 | 
			
		||||
					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);
 | 
			
		||||
						console.log(data);
 | 
			
		||||
					let lines = value.split('\n');
 | 
			
		||||
 | 
			
		||||
					for (const line of lines) {
 | 
			
		||||
						if (line !== '') {
 | 
			
		||||
							let data = JSON.parse(line);
 | 
			
		||||
						if (data.error) {
 | 
			
		||||
							throw data.error;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						if (data.detail) {
 | 
			
		||||
							throw data.detail;
 | 
			
		||||
						}
 | 
			
		||||
						if (data.status) {
 | 
			
		||||
							if (!data.digest) {
 | 
			
		||||
								if (data.status === 'success') {
 | 
			
		||||
									const notification = new Notification(`Ollama`, {
 | 
			
		||||
										body: `Model '${opts.modelName}' has been successfully downloaded.`,
 | 
			
		||||
										icon: '/favicon.png'
 | 
			
		||||
									});
 | 
			
		||||
								}
 | 
			
		||||
							} else {
 | 
			
		||||
								digest = data.digest;
 | 
			
		||||
							if (data.digest) {
 | 
			
		||||
								let downloadProgress = 0;
 | 
			
		||||
								if (data.completed) {
 | 
			
		||||
									downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
 | 
			
		||||
| 
						 | 
				
			
			@ -286,15 +286,21 @@
 | 
			
		|||
								modelDownloadStatus[opts.modelName] = {pullProgress: downloadProgress, digest: data.digest};
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				} catch (error) {
 | 
			
		||||
					console.log('Failed to read from data stream', error);
 | 
			
		||||
					throw error;
 | 
			
		||||
				}
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error(error);
 | 
			
		||||
				opts.callback({success:false, error, modelName: opts.modelName});
 | 
			
		||||
			}
 | 
			
		||||
			opts.callback({success: true, modelName: opts.modelName});
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error(error);
 | 
			
		||||
			opts.callback({success:false, error, modelName: opts.modelName});
 | 
			
		||||
		}
 | 
			
		||||
		opts.callback({success: true, modelName: opts.modelName});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
		
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
	const pullModelHandler = async() => {
 | 
			
		||||
		if(modelDownloadStatus[modelTag]){
 | 
			
		||||
| 
						 | 
				
			
			@ -437,21 +443,11 @@
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if (uploaded) {
 | 
			
		||||
			const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/create`, {
 | 
			
		||||
				method: 'POST',
 | 
			
		||||
				headers: {
 | 
			
		||||
					'Content-Type': 'text/event-stream',
 | 
			
		||||
					...($settings.authHeader && { Authorization: $settings.authHeader }),
 | 
			
		||||
					...($user && { Authorization: `Bearer ${localStorage.token}` })
 | 
			
		||||
				},
 | 
			
		||||
				body: JSON.stringify({
 | 
			
		||||
					name: `${name}:latest`,
 | 
			
		||||
					modelfile: `FROM @${modelFileDigest}\n${modelFileContent}`
 | 
			
		||||
				})
 | 
			
		||||
			}).catch((err) => {
 | 
			
		||||
				console.log(err);
 | 
			
		||||
				return null;
 | 
			
		||||
			});
 | 
			
		||||
			const res = await createModel(
 | 
			
		||||
				localStorage.token,
 | 
			
		||||
				`${name}:latest`,
 | 
			
		||||
				`FROM @${modelFileDigest}\n${modelFileContent}`
 | 
			
		||||
			);
 | 
			
		||||
 | 
			
		||||
			if (res && res.ok) {
 | 
			
		||||
				const reader = res.body
 | 
			
		||||
| 
						 | 
				
			
			@ -517,124 +513,35 @@
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	const deleteModelHandler = async () => {
 | 
			
		||||
		const res = await fetch(`${API_BASE_URL}/delete`, {
 | 
			
		||||
			method: 'DELETE',
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'text/event-stream',
 | 
			
		||||
				...($settings.authHeader && { Authorization: $settings.authHeader }),
 | 
			
		||||
				...($user && { Authorization: `Bearer ${localStorage.token}` })
 | 
			
		||||
			},
 | 
			
		||||
			body: JSON.stringify({
 | 
			
		||||
				name: deleteModelTag
 | 
			
		||||
			})
 | 
			
		||||
		const res = await deleteModel(localStorage.token, deleteModelTag).catch((error) => {
 | 
			
		||||
			toast.error(error);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		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 !== '' && line !== 'null') {
 | 
			
		||||
						console.log(line);
 | 
			
		||||
						let data = JSON.parse(line);
 | 
			
		||||
						console.log(data);
 | 
			
		||||
 | 
			
		||||
						if (data.error) {
 | 
			
		||||
							throw data.error;
 | 
			
		||||
						}
 | 
			
		||||
						if (data.detail) {
 | 
			
		||||
							throw data.detail;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						if (data.status) {
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						toast.success(`Deleted ${deleteModelTag}`);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.log(error);
 | 
			
		||||
				toast.error(error);
 | 
			
		||||
			}
 | 
			
		||||
		if (res) {
 | 
			
		||||
			toast.success(`Deleted ${deleteModelTag}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deleteModelTag = '';
 | 
			
		||||
		models.set(await getModels());
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const getModels = async (url = '', type = 'all') => {
 | 
			
		||||
		let models = [];
 | 
			
		||||
		const res = await fetch(`${url ? url : $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, {
 | 
			
		||||
			method: 'GET',
 | 
			
		||||
			headers: {
 | 
			
		||||
				Accept: 'application/json',
 | 
			
		||||
				'Content-Type': 'application/json',
 | 
			
		||||
				...($settings.authHeader && { Authorization: $settings.authHeader }),
 | 
			
		||||
				...($user && { Authorization: `Bearer ${localStorage.token}` })
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
			.then(async (res) => {
 | 
			
		||||
				if (!res.ok) throw await res.json();
 | 
			
		||||
				return res.json();
 | 
			
		||||
			})
 | 
			
		||||
			.catch((error) => {
 | 
			
		||||
				console.log(error);
 | 
			
		||||
				if ('detail' in error) {
 | 
			
		||||
					toast.error(error.detail);
 | 
			
		||||
				} else {
 | 
			
		||||
					toast.error('Server connection failed');
 | 
			
		||||
				}
 | 
			
		||||
				return null;
 | 
			
		||||
			});
 | 
			
		||||
		console.log(res);
 | 
			
		||||
		models.push(...(res?.models ?? []));
 | 
			
		||||
	const getModels = async (type = 'all') => {
 | 
			
		||||
		const models = [];
 | 
			
		||||
		models.push(
 | 
			
		||||
			...(await getOllamaModels(localStorage.token).catch((error) => {
 | 
			
		||||
				toast.error(error);
 | 
			
		||||
				return [];
 | 
			
		||||
			}))
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		// If OpenAI API Key exists
 | 
			
		||||
		if (type === 'all' && $settings.OPENAI_API_KEY) {
 | 
			
		||||
			const API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
 | 
			
		||||
		if (type === 'all' && OPENAI_API_KEY) {
 | 
			
		||||
			const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => {
 | 
			
		||||
				console.log(error);
 | 
			
		||||
				return null;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Validate OPENAI_API_KEY
 | 
			
		||||
			const openaiModelRes = await fetch(`${API_BASE_URL}/models`, {
 | 
			
		||||
				method: 'GET',
 | 
			
		||||
				headers: {
 | 
			
		||||
					'Content-Type': 'application/json',
 | 
			
		||||
					Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
				.then(async (res) => {
 | 
			
		||||
					if (!res.ok) throw await res.json();
 | 
			
		||||
					return res.json();
 | 
			
		||||
				})
 | 
			
		||||
				.catch((error) => {
 | 
			
		||||
					console.log(error);
 | 
			
		||||
					toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
 | 
			
		||||
					return null;
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
			const openAIModels = Array.isArray(openaiModelRes)
 | 
			
		||||
				? openaiModelRes
 | 
			
		||||
				: openaiModelRes?.data ?? null;
 | 
			
		||||
 | 
			
		||||
			models.push(
 | 
			
		||||
				...(openAIModels
 | 
			
		||||
					? [
 | 
			
		||||
							{ name: 'hr' },
 | 
			
		||||
							...openAIModels
 | 
			
		||||
								.map((model) => ({ name: model.id, external: true }))
 | 
			
		||||
								.filter((model) =>
 | 
			
		||||
									API_BASE_URL.includes('openai') ? model.name.includes('gpt') : true
 | 
			
		||||
								)
 | 
			
		||||
					  ]
 | 
			
		||||
					: [])
 | 
			
		||||
			);
 | 
			
		||||
			models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return models;
 | 
			
		||||
| 
						 | 
				
			
			@ -666,15 +573,20 @@
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		console.log('settings', $user.role === 'admin');
 | 
			
		||||
		if ($user.role === 'admin') {
 | 
			
		||||
			API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
 | 
			
		||||
			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
 | 
			
		||||
			OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
 | 
			
		||||
		console.log(settings);
 | 
			
		||||
 | 
			
		||||
		theme = localStorage.theme ?? 'dark';
 | 
			
		||||
		notificationEnabled = settings.notificationEnabled ?? false;
 | 
			
		||||
 | 
			
		||||
		API_BASE_URL = settings.API_BASE_URL ?? OLLAMA_API_BASE_URL;
 | 
			
		||||
		system = settings.system ?? '';
 | 
			
		||||
 | 
			
		||||
		requestFormat = settings.requestFormat ?? '';
 | 
			
		||||
 | 
			
		||||
		options.seed = settings.seed ?? 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -689,10 +601,10 @@
 | 
			
		|||
		titleAutoGenerate = settings.titleAutoGenerate ?? true;
 | 
			
		||||
		speechAutoSend = settings.speechAutoSend ?? false;
 | 
			
		||||
		responseAutoCopy = settings.responseAutoCopy ?? false;
 | 
			
		||||
 | 
			
		||||
		titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
 | 
			
		||||
		gravatarEmail = settings.gravatarEmail ?? '';
 | 
			
		||||
		OPENAI_API_KEY = settings.OPENAI_API_KEY ?? '';
 | 
			
		||||
		OPENAI_API_BASE_URL = settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
 | 
			
		||||
 | 
			
		||||
		saveChatHistory = settings.saveChatHistory ?? true;
 | 
			
		||||
 | 
			
		||||
		authEnabled = settings.authHeader !== undefined ? true : false;
 | 
			
		||||
		if (authEnabled) {
 | 
			
		||||
| 
						 | 
				
			
			@ -700,10 +612,7 @@
 | 
			
		|||
			authContent = settings.authHeader.split(' ')[1];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ollamaVersion = await getOllamaVersion(
 | 
			
		||||
			API_BASE_URL ?? OLLAMA_API_BASE_URL,
 | 
			
		||||
			localStorage.token
 | 
			
		||||
		).catch((error) => {
 | 
			
		||||
		ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
 | 
			
		||||
			return '';
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
| 
						 | 
				
			
			@ -787,55 +696,57 @@
 | 
			
		|||
					<div class=" self-center">Advanced</div>
 | 
			
		||||
				</button>
 | 
			
		||||
 | 
			
		||||
				<button
 | 
			
		||||
					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 | 
			
		||||
					'models'
 | 
			
		||||
						? 'bg-gray-200 dark:bg-gray-700'
 | 
			
		||||
						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						selectedTab = 'models';
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<div class=" self-center mr-2">
 | 
			
		||||
						<svg
 | 
			
		||||
							xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
							viewBox="0 0 20 20"
 | 
			
		||||
							fill="currentColor"
 | 
			
		||||
							class="w-4 h-4"
 | 
			
		||||
						>
 | 
			
		||||
							<path
 | 
			
		||||
								fill-rule="evenodd"
 | 
			
		||||
								d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
 | 
			
		||||
								clip-rule="evenodd"
 | 
			
		||||
							/>
 | 
			
		||||
						</svg>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class=" self-center">Models</div>
 | 
			
		||||
				</button>
 | 
			
		||||
				{#if $user?.role === 'admin'}
 | 
			
		||||
					<button
 | 
			
		||||
						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 | 
			
		||||
						'models'
 | 
			
		||||
							? 'bg-gray-200 dark:bg-gray-700'
 | 
			
		||||
							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							selectedTab = 'models';
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<div class=" self-center mr-2">
 | 
			
		||||
							<svg
 | 
			
		||||
								xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
								viewBox="0 0 20 20"
 | 
			
		||||
								fill="currentColor"
 | 
			
		||||
								class="w-4 h-4"
 | 
			
		||||
							>
 | 
			
		||||
								<path
 | 
			
		||||
									fill-rule="evenodd"
 | 
			
		||||
									d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
 | 
			
		||||
									clip-rule="evenodd"
 | 
			
		||||
								/>
 | 
			
		||||
							</svg>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class=" self-center">Models</div>
 | 
			
		||||
					</button>
 | 
			
		||||
 | 
			
		||||
				<button
 | 
			
		||||
					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 | 
			
		||||
					'external'
 | 
			
		||||
						? 'bg-gray-200 dark:bg-gray-700'
 | 
			
		||||
						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 | 
			
		||||
					on:click={() => {
 | 
			
		||||
						selectedTab = 'external';
 | 
			
		||||
					}}
 | 
			
		||||
				>
 | 
			
		||||
					<div class=" self-center mr-2">
 | 
			
		||||
						<svg
 | 
			
		||||
							xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
							viewBox="0 0 16 16"
 | 
			
		||||
							fill="currentColor"
 | 
			
		||||
							class="w-4 h-4"
 | 
			
		||||
						>
 | 
			
		||||
							<path
 | 
			
		||||
								d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
 | 
			
		||||
							/>
 | 
			
		||||
						</svg>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class=" self-center">External</div>
 | 
			
		||||
				</button>
 | 
			
		||||
					<button
 | 
			
		||||
						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 | 
			
		||||
						'external'
 | 
			
		||||
							? 'bg-gray-200 dark:bg-gray-700'
 | 
			
		||||
							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
 | 
			
		||||
						on:click={() => {
 | 
			
		||||
							selectedTab = 'external';
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
						<div class=" self-center mr-2">
 | 
			
		||||
							<svg
 | 
			
		||||
								xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
								viewBox="0 0 16 16"
 | 
			
		||||
								fill="currentColor"
 | 
			
		||||
								class="w-4 h-4"
 | 
			
		||||
							>
 | 
			
		||||
								<path
 | 
			
		||||
									d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
 | 
			
		||||
								/>
 | 
			
		||||
							</svg>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class=" self-center">External</div>
 | 
			
		||||
					</button>
 | 
			
		||||
				{/if}
 | 
			
		||||
 | 
			
		||||
				<button
 | 
			
		||||
					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 | 
			
		||||
| 
						 | 
				
			
			@ -1065,51 +976,51 @@
 | 
			
		|||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<hr class=" dark:border-gray-700" />
 | 
			
		||||
						<div>
 | 
			
		||||
							<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
 | 
			
		||||
							<div class="flex w-full">
 | 
			
		||||
								<div class="flex-1 mr-2">
 | 
			
		||||
									<input
 | 
			
		||||
										class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
 | 
			
		||||
										placeholder="Enter URL (e.g. http://localhost:8080/ollama/api)"
 | 
			
		||||
										bind:value={API_BASE_URL}
 | 
			
		||||
									/>
 | 
			
		||||
								</div>
 | 
			
		||||
								<button
 | 
			
		||||
									class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
 | 
			
		||||
									on:click={() => {
 | 
			
		||||
										checkOllamaConnection();
 | 
			
		||||
									}}
 | 
			
		||||
								>
 | 
			
		||||
									<svg
 | 
			
		||||
										xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
										viewBox="0 0 20 20"
 | 
			
		||||
										fill="currentColor"
 | 
			
		||||
										class="w-4 h-4"
 | 
			
		||||
									>
 | 
			
		||||
										<path
 | 
			
		||||
											fill-rule="evenodd"
 | 
			
		||||
											d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
 | 
			
		||||
											clip-rule="evenodd"
 | 
			
		||||
						{#if $user.role === 'admin'}
 | 
			
		||||
							<hr class=" dark:border-gray-700" />
 | 
			
		||||
							<div>
 | 
			
		||||
								<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
 | 
			
		||||
								<div class="flex w-full">
 | 
			
		||||
									<div class="flex-1 mr-2">
 | 
			
		||||
										<input
 | 
			
		||||
											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
 | 
			
		||||
											placeholder="Enter URL (e.g. http://localhost:11434/api)"
 | 
			
		||||
											bind:value={API_BASE_URL}
 | 
			
		||||
										/>
 | 
			
		||||
									</svg>
 | 
			
		||||
								</button>
 | 
			
		||||
							</div>
 | 
			
		||||
									</div>
 | 
			
		||||
									<button
 | 
			
		||||
										class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
 | 
			
		||||
										on:click={() => {
 | 
			
		||||
											updateOllamaAPIUrlHandler();
 | 
			
		||||
										}}
 | 
			
		||||
									>
 | 
			
		||||
										<svg
 | 
			
		||||
											xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
											viewBox="0 0 20 20"
 | 
			
		||||
											fill="currentColor"
 | 
			
		||||
											class="w-4 h-4"
 | 
			
		||||
										>
 | 
			
		||||
											<path
 | 
			
		||||
												fill-rule="evenodd"
 | 
			
		||||
												d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
 | 
			
		||||
												clip-rule="evenodd"
 | 
			
		||||
											/>
 | 
			
		||||
										</svg>
 | 
			
		||||
									</button>
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
							<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
 | 
			
		||||
								The field above should be set to <span
 | 
			
		||||
									class=" text-gray-500 dark:text-gray-300 font-medium">'/ollama/api'</span
 | 
			
		||||
								>;
 | 
			
		||||
								<a
 | 
			
		||||
									class=" text-gray-500 dark:text-gray-300 font-medium"
 | 
			
		||||
									href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
 | 
			
		||||
									target="_blank"
 | 
			
		||||
								>
 | 
			
		||||
									Click here for help.
 | 
			
		||||
								</a>
 | 
			
		||||
								<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
 | 
			
		||||
									Trouble accessing Ollama?
 | 
			
		||||
									<a
 | 
			
		||||
										class=" text-gray-300 font-medium"
 | 
			
		||||
										href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
 | 
			
		||||
										target="_blank"
 | 
			
		||||
									>
 | 
			
		||||
										Click here for help.
 | 
			
		||||
									</a>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						{/if}
 | 
			
		||||
 | 
			
		||||
						<hr class=" dark:border-gray-700" />
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1127,7 +1038,6 @@
 | 
			
		|||
								class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
 | 
			
		||||
								on:click={() => {
 | 
			
		||||
									saveSettings({
 | 
			
		||||
										API_BASE_URL: API_BASE_URL === '' ? OLLAMA_API_BASE_URL : API_BASE_URL,
 | 
			
		||||
										system: system !== '' ? system : undefined
 | 
			
		||||
									});
 | 
			
		||||
									show = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -1548,10 +1458,12 @@
 | 
			
		|||
					<form
 | 
			
		||||
						class="flex flex-col h-full justify-between space-y-3 text-sm"
 | 
			
		||||
						on:submit|preventDefault={() => {
 | 
			
		||||
							saveSettings({
 | 
			
		||||
								OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
 | 
			
		||||
								OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
 | 
			
		||||
							});
 | 
			
		||||
							updateOpenAIHandler();
 | 
			
		||||
 | 
			
		||||
							// saveSettings({
 | 
			
		||||
							// 	OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
 | 
			
		||||
							// 	OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
 | 
			
		||||
							// });
 | 
			
		||||
							show = false;
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
| 
						 | 
				
			
			@ -1608,10 +1520,6 @@
 | 
			
		|||
					<form
 | 
			
		||||
						class="flex flex-col h-full justify-between space-y-3 text-sm"
 | 
			
		||||
						on:submit|preventDefault={() => {
 | 
			
		||||
							saveSettings({
 | 
			
		||||
								gravatarEmail: gravatarEmail !== '' ? gravatarEmail : undefined,
 | 
			
		||||
								gravatarUrl: gravatarEmail !== '' ? getGravatarURL(gravatarEmail) : undefined
 | 
			
		||||
							});
 | 
			
		||||
							show = false;
 | 
			
		||||
						}}
 | 
			
		||||
					>
 | 
			
		||||
| 
						 | 
				
			
			@ -1621,7 +1529,7 @@
 | 
			
		|||
 | 
			
		||||
								<div>
 | 
			
		||||
									<div class=" py-0.5 flex w-full justify-between">
 | 
			
		||||
										<div class=" self-center text-xs font-medium">Title Auto Generation</div>
 | 
			
		||||
										<div class=" self-center text-xs font-medium">Title Auto-Generation</div>
 | 
			
		||||
 | 
			
		||||
										<button
 | 
			
		||||
											class="p-1 px-3 text-xs flex rounded transition"
 | 
			
		||||
| 
						 | 
				
			
			@ -1683,6 +1591,54 @@
 | 
			
		|||
							</div>
 | 
			
		||||
 | 
			
		||||
							<hr class=" dark:border-gray-700" />
 | 
			
		||||
 | 
			
		||||
							<div>
 | 
			
		||||
								<div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div>
 | 
			
		||||
								<div class="flex w-full">
 | 
			
		||||
									<div class="flex-1 mr-2">
 | 
			
		||||
										<select
 | 
			
		||||
											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
 | 
			
		||||
											bind:value={titleAutoGenerateModel}
 | 
			
		||||
											placeholder="Select a model"
 | 
			
		||||
										>
 | 
			
		||||
											<option value="" selected>Default</option>
 | 
			
		||||
											{#each $models.filter((m) => m.size != null) as model}
 | 
			
		||||
												<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
 | 
			
		||||
													>{model.name +
 | 
			
		||||
														' (' +
 | 
			
		||||
														(model.size / 1024 ** 3).toFixed(1) +
 | 
			
		||||
														' GB)'}</option
 | 
			
		||||
												>
 | 
			
		||||
											{/each}
 | 
			
		||||
										</select>
 | 
			
		||||
									</div>
 | 
			
		||||
									<button
 | 
			
		||||
										class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
 | 
			
		||||
										on:click={() => {
 | 
			
		||||
											saveSettings({
 | 
			
		||||
												titleAutoGenerateModel:
 | 
			
		||||
													titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined
 | 
			
		||||
											});
 | 
			
		||||
										}}
 | 
			
		||||
										type="button"
 | 
			
		||||
									>
 | 
			
		||||
										<svg
 | 
			
		||||
											xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
											viewBox="0 0 16 16"
 | 
			
		||||
											fill="currentColor"
 | 
			
		||||
											class="w-3.5 h-3.5"
 | 
			
		||||
										>
 | 
			
		||||
											<path
 | 
			
		||||
												fill-rule="evenodd"
 | 
			
		||||
												d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
 | 
			
		||||
												clip-rule="evenodd"
 | 
			
		||||
											/>
 | 
			
		||||
										</svg>
 | 
			
		||||
									</button>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
 | 
			
		||||
							<!-- <hr class=" dark:border-gray-700" />
 | 
			
		||||
							<div>
 | 
			
		||||
								<div class=" mb-2.5 text-sm font-medium">
 | 
			
		||||
									Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -1705,7 +1661,7 @@
 | 
			
		|||
										target="_blank">Gravatar.</a
 | 
			
		||||
									>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
							</div> -->
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div class="flex justify-end pt-3 text-sm font-medium">
 | 
			
		||||
| 
						 | 
				
			
			@ -1720,6 +1676,64 @@
 | 
			
		|||
				{:else if selectedTab === 'chats'}
 | 
			
		||||
					<div class="flex flex-col h-full justify-between space-y-3 text-sm">
 | 
			
		||||
						<div class=" space-y-2">
 | 
			
		||||
							<div
 | 
			
		||||
								class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
 | 
			
		||||
							>
 | 
			
		||||
								<div class="flex w-full justify-between">
 | 
			
		||||
									<div class=" self-center text-sm font-medium">Chat History</div>
 | 
			
		||||
 | 
			
		||||
									<button
 | 
			
		||||
										class="p-1 px-3 text-xs flex rounded transition"
 | 
			
		||||
										type="button"
 | 
			
		||||
										on:click={() => {
 | 
			
		||||
											toggleSaveChatHistory();
 | 
			
		||||
										}}
 | 
			
		||||
									>
 | 
			
		||||
										{#if saveChatHistory === true}
 | 
			
		||||
											<svg
 | 
			
		||||
												xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
												viewBox="0 0 16 16"
 | 
			
		||||
												fill="currentColor"
 | 
			
		||||
												class="w-4 h-4"
 | 
			
		||||
											>
 | 
			
		||||
												<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
 | 
			
		||||
												<path
 | 
			
		||||
													fill-rule="evenodd"
 | 
			
		||||
													d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
 | 
			
		||||
													clip-rule="evenodd"
 | 
			
		||||
												/>
 | 
			
		||||
											</svg>
 | 
			
		||||
 | 
			
		||||
											<span class="ml-2 self-center"> On </span>
 | 
			
		||||
										{:else}
 | 
			
		||||
											<svg
 | 
			
		||||
												xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
												viewBox="0 0 16 16"
 | 
			
		||||
												fill="currentColor"
 | 
			
		||||
												class="w-4 h-4"
 | 
			
		||||
											>
 | 
			
		||||
												<path
 | 
			
		||||
													fill-rule="evenodd"
 | 
			
		||||
													d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
 | 
			
		||||
													clip-rule="evenodd"
 | 
			
		||||
												/>
 | 
			
		||||
												<path
 | 
			
		||||
													d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
 | 
			
		||||
												/>
 | 
			
		||||
											</svg>
 | 
			
		||||
 | 
			
		||||
											<span class="ml-2 self-center">Off</span>
 | 
			
		||||
										{/if}
 | 
			
		||||
									</button>
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
								<div class="text-xs text-left w-full font-medium mt-0.5">
 | 
			
		||||
									This setting does not sync across browsers or devices.
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
 | 
			
		||||
							<hr class=" dark:border-gray-700" />
 | 
			
		||||
 | 
			
		||||
							<div class="flex flex-col">
 | 
			
		||||
								<input
 | 
			
		||||
									id="chat-import-input"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue