"vscode:/vscode.git/clone" did not exist on "00f32e26516714ef115d908559d97547f62bae24"
Commit 17a6ca50 authored by Anuraag Jain's avatar Anuraag Jain
Browse files

Merge branch 'main' of https://github.com/anuraagdjain/ollama-webui into...

Merge branch 'main' of https://github.com/anuraagdjain/ollama-webui into feat/parallel-model-downloads
parents fd0dcec6 c55c8728
import { OLLAMA_API_BASE_URL } from '$lib/constants';
export const getOllamaVersion = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string = ''
) => {
export const getOllamaAPIUrl = async (token: string = '') => {
let error = null;
const res = await fetch(`${base_url}/version`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/url`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.OLLAMA_API_BASE_URL;
};
export const updateOllamaAPIUrl = async (token: string = '', url: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/url/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
url: url
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.OLLAMA_API_BASE_URL;
};
export const getOllamaVersion = async (token: string = '') => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/version`, {
method: 'GET',
headers: {
Accept: 'application/json',
......@@ -35,13 +99,10 @@ export const getOllamaVersion = async (
return res?.version ?? '';
};
export const getOllamaModels = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string = ''
) => {
export const getOllamaModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${base_url}/tags`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, {
method: 'GET',
headers: {
Accept: 'application/json',
......@@ -67,18 +128,15 @@ export const getOllamaModels = async (
throw error;
}
return res?.models ?? [];
return (res?.models ?? []).sort((a, b) => {
return a.name.localeCompare(b.name);
});
};
export const generateTitle = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string = '',
model: string,
prompt: string
) => {
export const generateTitle = async (token: string = '', model: string, prompt: string) => {
let error = null;
const res = await fetch(`${base_url}/generate`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
......@@ -86,7 +144,7 @@ export const generateTitle = async (
},
body: JSON.stringify({
model: model,
prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${prompt}`,
prompt: `Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': ${prompt}`,
stream: false
})
})
......@@ -109,14 +167,10 @@ export const generateTitle = async (
return res?.response ?? 'New Chat';
};
export const generateChatCompletion = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string = '',
body: object
) => {
export const generateChatCompletion = async (token: string = '', body: object) => {
let error = null;
const res = await fetch(`${base_url}/chat`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
......@@ -135,15 +189,10 @@ export const generateChatCompletion = async (
return res;
};
export const createModel = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string,
tagName: string,
content: string
) => {
export const createModel = async (token: string, tagName: string, content: string) => {
let error = null;
const res = await fetch(`${base_url}/create`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
......@@ -165,14 +214,10 @@ export const createModel = async (
return res;
};
export const deleteModel = async (
base_url: string = OLLAMA_API_BASE_URL,
token: string,
tagName: string
) => {
export const deleteModel = async (token: string, tagName: string) => {
let error = null;
const res = await fetch(`${base_url}/delete`, {
const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, {
method: 'DELETE',
headers: {
'Content-Type': 'text/event-stream',
......@@ -202,3 +247,27 @@ export const deleteModel = async (
return res;
};
export const pullModel = async (token: string, tagName: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
method: 'POST',
headers: {
'Content-Type': 'text/event-stream',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
name: tagName
})
}).catch((err) => {
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const getOpenAIModels = async (
import { OPENAI_API_BASE_URL } from '$lib/constants';
export const getOpenAIUrl = async (token: string = '') => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/url`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.OPENAI_API_BASE_URL;
};
export const updateOpenAIUrl = async (token: string = '', url: string) => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
url: url
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.OPENAI_API_BASE_URL;
};
export const getOpenAIKey = async (token: string = '') => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/key`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.OPENAI_API_KEY;
};
export const updateOpenAIKey = async (token: string = '', key: string) => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
key: key
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.OPENAI_API_KEY;
};
export const getOpenAIModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/models`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
const models = Array.isArray(res) ? res : res?.data ?? null;
return models
? models
.map((model) => ({ name: model.id, external: true }))
.sort((a, b) => {
return a.name.localeCompare(b.name);
})
: models;
};
export const getOpenAIModelsDirect = async (
base_url: string = 'https://api.openai.com/v1',
api_key: string = ''
) => {
......@@ -25,9 +197,35 @@ export const getOpenAIModels = async (
throw error;
}
let models = Array.isArray(res) ? res : res?.data ?? null;
const models = Array.isArray(res) ? res : res?.data ?? null;
return models
.map((model) => ({ name: model.id, external: true }))
.filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true));
.filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true))
.sort((a, b) => {
return a.name.localeCompare(b.name);
});
};
export const generateOpenAIChatCompletion = async (token: string = '', body: object) => {
let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}).catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const createNewPrompt = async (
token: string,
command: string,
title: string,
content: string
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/create`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
command: `/${command}`,
title: title,
content: content
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getPrompts = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getPromptByCommand = async (token: string, command: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const updatePromptByCommand = async (
token: string,
command: string,
title: string,
content: string
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
command: `/${command}`,
title: title,
content: content
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deletePromptByCommand = async (token: string, command: string) => {
let error = null;
command = command.charAt(0) === '/' ? command.slice(1) : command;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/${command}/delete`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
......@@ -84,3 +84,43 @@ export const deleteUserById = async (token: string, userId: string) => {
return res;
};
type UserUpdateForm = {
profile_image_url: string;
email: string;
name: string;
password: string;
};
export const updateUserById = async (token: string, userId: string, user: UserUpdateForm) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
profile_image_url: user.profile_image_url,
email: user.email,
name: user.name,
password: user.password !== '' ? user.password : undefined
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
<script lang="ts">
import toast from 'svelte-french-toast';
import dayjs from 'dayjs';
import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte';
import { updateUserById } from '$lib/apis/users';
import Modal from '../common/Modal.svelte';
const dispatch = createEventDispatcher();
export let show = false;
export let selectedUser;
export let sessionUser;
let _user = {
profile_image_url: '',
name: '',
email: '',
password: ''
};
const submitHandler = async () => {
const res = await updateUserById(localStorage.token, selectedUser.id, _user).catch((error) => {
toast.error(error);
});
if (res) {
dispatch('save');
show = false;
}
};
onMount(() => {
if (selectedUser) {
_user = selectedUser;
_user.password = '';
}
});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">Edit User</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<hr class=" dark:border-gray-800" />
<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class=" flex items-center rounded-md py-2 px-4 w-full">
<div class=" self-center mr-5">
<img
src={selectedUser.profile_image_url}
class=" max-w-[55px] object-cover rounded-full"
alt="User profile"
/>
</div>
<div>
<div class=" self-center capitalize font-semibold">{selectedUser.name}</div>
<div class="text-xs text-gray-500">
Created at {dayjs(selectedUser.timestamp * 1000).format('MMMM DD, YYYY')}
</div>
</div>
</div>
<hr class=" dark:border-gray-800 my-3 w-full" />
<div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Email</div>
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="email"
bind:value={_user.email}
autocomplete="off"
required
disabled={_user.id == sessionUser.id}
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Name</div>
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
type="text"
bind:value={_user.name}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">New Password</div>
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
type="password"
bind:value={_user.password}
autocomplete="new-password"
/>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>
......@@ -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) => {
......
<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'
......
<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">
......
......@@ -3,8 +3,18 @@
import { fade, blur } from 'svelte/transition';
export let show = true;
export let size = 'md';
let mounted = false;
const sizeToWidth = (size) => {
if (size === 'sm') {
return 'w-[30rem]';
} else {
return 'w-[40rem]';
}
};
onMount(() => {
mounted = true;
});
......@@ -28,7 +38,9 @@
}}
>
<div
class="m-auto rounded-xl max-w-full w-[40rem] mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
class="m-auto rounded-xl max-w-full {sizeToWidth(
size
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
transition:fade={{ delay: 100, duration: 200 }}
on:click={(e) => {
e.stopPropagation();
......
......@@ -6,7 +6,7 @@
import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
import { user, chats, showSettings, chatId } from '$lib/stores';
import { user, chats, settings, showSettings, chatId } from '$lib/stores';
import { onMount } from 'svelte';
import { deleteChatById, getChatList, updateChatById } from '$lib/apis/chats';
......@@ -49,6 +49,12 @@
await deleteChatById(localStorage.token, id);
await chats.set(await getChatList(localStorage.token));
};
const saveSettings = async (updated) => {
await settings.set({ ...$settings, ...updated });
localStorage.setItem('settings', JSON.stringify($settings));
location.href = '/';
};
</script>
<div
......@@ -100,7 +106,7 @@
</div>
{#if $user?.role === 'admin'}
<div class="px-2.5 flex justify-center my-1">
<div class="px-2.5 flex justify-center mt-1">
<button
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
on:click={async () => {
......@@ -129,255 +135,326 @@
</div>
</button>
</div>
{/if}
<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2">
<div class="flex w-full" id="chat-search">
<div class="self-center pl-3 py-2 rounded-l bg-gray-950">
<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="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class="w-full rounded-r py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
placeholder="Search"
bind:value={search}
/>
<div class="px-2.5 flex justify-center mb-1">
<button
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
on:click={async () => {
goto('/prompts');
}}
>
<div class="self-center">
<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="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
clip-rule="evenodd"
/>
</svg>
</div>
<!-- <div class="self-center pr-3 py-2 bg-gray-900">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
/>
</svg>
</div> -->
<div class="flex self-center">
<div class=" self-center font-medium text-sm">Prompts</div>
</div>
</button>
</div>
</div>
{/if}
<div class="pl-2.5 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto">
{#each $chats.filter((chat) => {
if (search === '') {
return true;
} else {
let title = chat.title.toLowerCase();
<div class="relative flex flex-col flex-1 overflow-y-auto">
{#if !($settings.saveChatHistory ?? true)}
<div class="absolute z-40 w-full h-full bg-black/90 flex justify-center">
<div class=" text-left px-5 py-2">
<div class=" font-medium">Chat History is off for this browser.</div>
<div class="text-xs mt-2">
When history is turned off, new chats on this browser won't appear in your history on
any of your devices. <span class=" font-semibold"
>This setting does not sync across browsers or devices.</span
>
</div>
if (title.includes(search)) {
return true;
} else {
return false;
}
}
}) as chat, i}
<div class=" w-full pr-2 relative">
<button
class=" w-full flex justify-between rounded-md px-3 py-2 hover:bg-gray-900 {chat.id ===
$chatId
? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis"
on:click={() => {
// goto(`/c/${chat.id}`);
if (chat.id !== chatTitleEditId) {
chatTitleEditId = null;
chatTitle = '';
}
if (chat.id !== $chatId) {
loadChat(chat.id);
}
}}
>
<div class=" flex self-center flex-1">
<div class=" self-center mr-3">
<div class="mt-3">
<button
class="flex justify-center items-center space-x-1.5 px-3 py-2.5 rounded-lg text-xs bg-gray-200 hover:bg-gray-300 transition text-gray-800 font-medium w-full"
type="button"
on:click={() => {
saveSettings({
saveChatHistory: true
});
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3 h-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
fill-rule="evenodd"
d="M8 1a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0v-6.5A.75.75 0 0 1 8 1ZM4.11 3.05a.75.75 0 0 1 0 1.06 5.5 5.5 0 1 0 7.78 0 .75.75 0 0 1 1.06-1.06 7 7 0 1 1-9.9 0 .75.75 0 0 1 1.06 0Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div
class=" text-left self-center overflow-hidden {chat.id === $chatId
? 'w-[120px]'
: 'w-[180px]'} "
>
{#if chatTitleEditId === chat.id}
<input bind:value={chatTitle} class=" bg-transparent w-full" />
{:else}
{chat.title}
{/if}
</div>
<div>Enable Chat History</div>
</button>
</div>
</button>
</div>
</div>
{/if}
{#if chat.id === $chatId}
<div class=" absolute right-[22px] top-[10px]">
{#if chatTitleEditId === chat.id}
<div class="flex self-center space-x-1.5">
<button
class=" self-center hover:text-white transition"
on:click={() => {
editChatTitle(chat.id, chatTitle);
chatTitleEditId = null;
chatTitle = '';
}}
<div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2">
<div class="flex w-full" id="chat-search">
<div class="self-center pl-3 py-2 rounded-l bg-gray-950">
<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="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class="w-full rounded-r py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
placeholder="Search"
bind:value={search}
/>
<!-- <div class="self-center pr-3 py-2 bg-gray-900">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
/>
</svg>
</div> -->
</div>
</div>
<div class="pl-2.5 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto">
{#each $chats.filter((chat) => {
if (search === '') {
return true;
} else {
let title = chat.title.toLowerCase();
if (title.includes(search)) {
return true;
} else {
return false;
}
}
}) as chat, i}
<div class=" w-full pr-2 relative">
<button
class=" w-full flex justify-between rounded-md px-3 py-2 hover:bg-gray-900 {chat.id ===
$chatId
? 'bg-gray-900'
: ''} transition whitespace-nowrap text-ellipsis"
on:click={() => {
// goto(`/c/${chat.id}`);
if (chat.id !== chatTitleEditId) {
chatTitleEditId = null;
chatTitle = '';
}
if (chat.id !== $chatId) {
loadChat(chat.id);
}
}}
>
<div class=" flex self-center flex-1">
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
/>
</svg>
</div>
<div
class=" text-left self-center overflow-hidden {chat.id === $chatId
? 'w-[120px]'
: 'w-[180px]'} "
>
{#if chatTitleEditId === chat.id}
<input bind:value={chatTitle} class=" bg-transparent w-full" />
{:else}
{chat.title}
{/if}
</div>
</div>
</button>
{#if chat.id === $chatId}
<div class=" absolute right-[22px] top-[10px]">
{#if chatTitleEditId === chat.id}
<div class="flex self-center space-x-1.5">
<button
class=" self-center hover:text-white transition"
on:click={() => {
editChatTitle(chat.id, chatTitle);
chatTitleEditId = null;
chatTitle = '';
}}
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatTitleEditId = null;
chatTitle = '';
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
<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="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatTitleEditId = null;
chatTitle = '';
}}
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
{:else if chatDeleteId === chat.id}
<div class="flex self-center space-x-1.5">
<button
class=" self-center hover:text-white transition"
on:click={() => {
deleteChat(chat.id);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
{:else if chatDeleteId === chat.id}
<div class="flex self-center space-x-1.5">
<button
class=" self-center hover:text-white transition"
on:click={() => {
deleteChat(chat.id);
}}
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatDeleteId = null;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
<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="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
</button>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatDeleteId = null;
}}
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
{:else}
<div class="flex self-center space-x-1.5">
<button
id="delete-chat-button"
class=" hidden"
on:click={() => {
deleteChat(chat.id);
}}
/>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatTitle = chat.title;
chatTitleEditId = chat.id;
// editChatTitle(chat.id, 'a');
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
{:else}
<div class="flex self-center space-x-1.5">
<button
id="delete-chat-button"
class=" hidden"
on:click={() => {
deleteChat(chat.id);
}}
/>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatTitle = chat.title;
chatTitleEditId = chat.id;
// editChatTitle(chat.id, 'a');
}}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatDeleteId = chat.id;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<button
class=" self-center hover:text-white transition"
on:click={() => {
chatDeleteId = chat.id;
}}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</div>
{/if}
</div>
{/if}
</div>
{/each}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</div>
{/if}
</div>
{/if}
</div>
{/each}
</div>
</div>
<div class="px-2.5">
......
import { dev, browser } from '$app/environment';
import { PUBLIC_API_BASE_URL } from '$env/static/public';
export const OLLAMA_API_BASE_URL = dev
? `http://${location.hostname}:8080/ollama/api`
: PUBLIC_API_BASE_URL === ''
? browser
? `http://${location.hostname}:11434/api`
: `http://localhost:11434/api`
: PUBLIC_API_BASE_URL;
import { dev } from '$app/environment';
export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`;
export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
......
......@@ -6,10 +6,13 @@ export const user = writable(undefined);
// Frontend
export const theme = writable('dark');
export const chatId = writable('');
export const chats = writable([]);
export const models = writable([]);
export const modelfiles = writable([]);
export const prompts = writable([]);
export const settings = writable({});
export const showSettings = writable(false);
......@@ -21,7 +21,7 @@ export const splitStream = (splitOn) => {
};
export const convertMessagesToHistory = (messages) => {
let history = {
const history = {
messages: {},
currentId: null
};
......@@ -114,7 +114,7 @@ export const checkVersion = (required, current) => {
export const findWordIndices = (text) => {
const regex = /\[([^\]]+)\]/g;
let matches = [];
const matches = [];
let match;
while ((match = regex.exec(text)) !== null) {
......
......@@ -9,11 +9,12 @@
import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
import { getModelfiles } from '$lib/apis/modelfiles';
import { getPrompts } from '$lib/apis/prompts';
import { getOpenAIModels } from '$lib/apis/openai';
import { user, showSettings, settings, models, modelfiles } from '$lib/stores';
import { OLLAMA_API_BASE_URL, REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
import { user, showSettings, settings, models, modelfiles, prompts } from '$lib/stores';
import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
import Sidebar from '$lib/components/layout/Sidebar.svelte';
......@@ -31,36 +32,28 @@
const getModels = async () => {
let models = [];
models.push(
...(await getOllamaModels(
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
localStorage.token
).catch((error) => {
...(await getOllamaModels(localStorage.token).catch((error) => {
toast.error(error);
return [];
}))
);
// If OpenAI API Key exists
if ($settings.OPENAI_API_KEY) {
const openAIModels = await getOpenAIModels(
$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1',
$settings.OPENAI_API_KEY
).catch((error) => {
console.log(error);
toast.error(error);
return null;
});
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
}
// $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1',
// $settings.OPENAI_API_KEY
const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => {
console.log(error);
return null;
});
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
return models;
};
const setOllamaVersion = async (version: string = '') => {
if (version === '') {
version = await getOllamaVersion(
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
localStorage.token
).catch((error) => {
version = await getOllamaVersion(localStorage.token).catch((error) => {
return '';
});
}
......@@ -101,6 +94,9 @@
console.log();
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
await modelfiles.set(await getModelfiles(localStorage.token));
await prompts.set(await getPrompts(localStorage.token));
console.log($modelfiles);
modelfiles.subscribe(async () => {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment