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'; import { OLLAMA_API_BASE_URL } from '$lib/constants';
export const getOllamaVersion = async ( export const getOllamaAPIUrl = async (token: string = '') => {
base_url: string = OLLAMA_API_BASE_URL,
token: string = ''
) => {
let error = null; 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', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
...@@ -35,13 +99,10 @@ export const getOllamaVersion = async ( ...@@ -35,13 +99,10 @@ export const getOllamaVersion = async (
return res?.version ?? ''; return res?.version ?? '';
}; };
export const getOllamaModels = async ( export const getOllamaModels = async (token: string = '') => {
base_url: string = OLLAMA_API_BASE_URL,
token: string = ''
) => {
let error = null; let error = null;
const res = await fetch(`${base_url}/tags`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
...@@ -67,18 +128,15 @@ export const getOllamaModels = async ( ...@@ -67,18 +128,15 @@ export const getOllamaModels = async (
throw error; throw error;
} }
return res?.models ?? []; return (res?.models ?? []).sort((a, b) => {
return a.name.localeCompare(b.name);
});
}; };
export const generateTitle = async ( export const generateTitle = async (token: string = '', model: string, prompt: string) => {
base_url: string = OLLAMA_API_BASE_URL,
token: string = '',
model: string,
prompt: string
) => {
let error = null; let error = null;
const res = await fetch(`${base_url}/generate`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
...@@ -86,7 +144,7 @@ export const generateTitle = async ( ...@@ -86,7 +144,7 @@ export const generateTitle = async (
}, },
body: JSON.stringify({ body: JSON.stringify({
model: model, 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 stream: false
}) })
}) })
...@@ -109,14 +167,10 @@ export const generateTitle = async ( ...@@ -109,14 +167,10 @@ export const generateTitle = async (
return res?.response ?? 'New Chat'; return res?.response ?? 'New Chat';
}; };
export const generateChatCompletion = async ( export const generateChatCompletion = async (token: string = '', body: object) => {
base_url: string = OLLAMA_API_BASE_URL,
token: string = '',
body: object
) => {
let error = null; let error = null;
const res = await fetch(`${base_url}/chat`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
...@@ -135,15 +189,10 @@ export const generateChatCompletion = async ( ...@@ -135,15 +189,10 @@ export const generateChatCompletion = async (
return res; return res;
}; };
export const createModel = async ( export const createModel = async (token: string, tagName: string, content: string) => {
base_url: string = OLLAMA_API_BASE_URL,
token: string,
tagName: string,
content: string
) => {
let error = null; let error = null;
const res = await fetch(`${base_url}/create`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
...@@ -165,14 +214,10 @@ export const createModel = async ( ...@@ -165,14 +214,10 @@ export const createModel = async (
return res; return res;
}; };
export const deleteModel = async ( export const deleteModel = async (token: string, tagName: string) => {
base_url: string = OLLAMA_API_BASE_URL,
token: string,
tagName: string
) => {
let error = null; let error = null;
const res = await fetch(`${base_url}/delete`, { const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
...@@ -202,3 +247,27 @@ export const deleteModel = async ( ...@@ -202,3 +247,27 @@ export const deleteModel = async (
return res; 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', base_url: string = 'https://api.openai.com/v1',
api_key: string = '' api_key: string = ''
) => { ) => {
...@@ -25,9 +197,35 @@ export const getOpenAIModels = async ( ...@@ -25,9 +197,35 @@ export const getOpenAIModels = async (
throw error; throw error;
} }
let models = Array.isArray(res) ? res : res?.data ?? null; const models = Array.isArray(res) ? res : res?.data ?? null;
return models return models
.map((model) => ({ name: model.id, external: true })) .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) => { ...@@ -84,3 +84,43 @@ export const deleteUserById = async (token: string, userId: string) => {
return res; 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 @@ ...@@ -298,7 +298,7 @@
id="chat-textarea" id="chat-textarea"
class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled class=" dark:bg-gray-800 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled
? '' ? ''
: ' pl-4'} rounded-xl resize-none" : ' pl-4'} rounded-xl resize-none h-[48px]"
placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'} placeholder={speechRecognitionListening ? 'Listening...' : 'Send a message'}
bind:value={prompt} bind:value={prompt}
on:keypress={(e) => { on:keypress={(e) => {
......
<script lang="ts"> <script lang="ts">
import { prompts } from '$lib/stores';
import { findWordIndices } from '$lib/utils'; import { findWordIndices } from '$lib/utils';
import { tick } from 'svelte'; import { tick } from 'svelte';
export let prompt = ''; export let prompt = '';
let selectedCommandIdx = 0; 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 = []; let filteredPromptCommands = [];
$: filteredPromptCommands = promptCommands $: filteredPromptCommands = $prompts
.filter((p) => p.command.includes(prompt)) .filter((p) => p.command.includes(prompt))
.sort((a, b) => a.title.localeCompare(b.title)); .sort((a, b) => a.title.localeCompare(b.title));
...@@ -195,32 +50,61 @@ and mentions the following keywords ...@@ -195,32 +50,61 @@ and mentions the following keywords
<div class="md:px-2 mb-3 text-left w-full"> <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="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=" 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>
<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} <div class="max-h-60 flex flex-col w-full rounded-r-lg">
<button <div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx {#each filteredPromptCommands as command, commandIdx}
? ' bg-gray-100 selected-command-option-button' <button
: ''}" class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx
type="button" ? ' bg-gray-100 selected-command-option-button'
on:click={() => { : ''}"
confirmCommand(command); type="button"
}} on:click={() => {
on:mousemove={() => { confirmCommand(command);
selectedCommandIdx = commandIdx; }}
}} on:mousemove={() => {
on:focus={() => {}} selectedCommandIdx = commandIdx;
> }}
<div class=" font-medium text-black"> on:focus={() => {}}
{command.command} >
</div> <div class=" font-medium text-black">
{command.command}
<div class=" text-xs text-gray-600"> </div>
{command.title}
</div> <div class=" text-xs text-gray-600">
</button> {command.title}
{/each} </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> </div>
</div> </div>
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
{#each suggestionPrompts as prompt, promptIdx} {#each suggestionPrompts as prompt, promptIdx}
<div class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px]"> <div class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px]">
<button <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={() => { on:click={() => {
submitPrompt(prompt.content); submitPrompt(prompt.content);
}} }}
...@@ -17,7 +17,9 @@ ...@@ -17,7 +17,9 @@
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div> <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> <div class="text-sm text-gray-500">{prompt.title[1]}</div>
{:else} {: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} {/if}
</div> </div>
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
> >
{#if model in modelfiles} {#if model in modelfiles}
<img <img
src={modelfiles[model]?.imageUrl} src={modelfiles[model]?.imageUrl ?? '/ollama-dark.png'}
alt="modelfile" alt="modelfile"
class=" w-20 mb-2 rounded-full {models.length > 1 class=" w-20 mb-2 rounded-full {models.length > 1
? ' border-[5px] border-white dark:border-gray-800' ? ' border-[5px] border-white dark:border-gray-800'
......
<script lang="ts"> <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'; import toast from 'svelte-french-toast';
export let selectedModels = ['']; export let selectedModels = [''];
export let disabled = false; export let disabled = false;
const saveDefaultModel = () => { const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === ''); const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) { if (hasEmptyModel.length) {
toast.error('Choose a model before saving...'); toast.error('Choose a model before saving...');
...@@ -13,8 +15,19 @@ ...@@ -13,8 +15,19 @@
} }
settings.set({ ...$settings, models: selectedModels }); settings.set({ ...$settings, models: selectedModels });
localStorage.setItem('settings', JSON.stringify($settings)); 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'); 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> </script>
<div class="flex flex-col my-2"> <div class="flex flex-col my-2">
......
...@@ -3,8 +3,18 @@ ...@@ -3,8 +3,18 @@
import { fade, blur } from 'svelte/transition'; import { fade, blur } from 'svelte/transition';
export let show = true; export let show = true;
export let size = 'md';
let mounted = false; let mounted = false;
const sizeToWidth = (size) => {
if (size === 'sm') {
return 'w-[30rem]';
} else {
return 'w-[40rem]';
}
};
onMount(() => { onMount(() => {
mounted = true; mounted = true;
}); });
...@@ -28,7 +38,9 @@ ...@@ -28,7 +38,9 @@
}} }}
> >
<div <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 }} transition:fade={{ delay: 100, duration: 200 }}
on:click={(e) => { on:click={(e) => {
e.stopPropagation(); e.stopPropagation();
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
import { goto, invalidateAll } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores'; 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 { onMount } from 'svelte';
import { deleteChatById, getChatList, updateChatById } from '$lib/apis/chats'; import { deleteChatById, getChatList, updateChatById } from '$lib/apis/chats';
...@@ -49,6 +49,12 @@ ...@@ -49,6 +49,12 @@
await deleteChatById(localStorage.token, id); await deleteChatById(localStorage.token, id);
await chats.set(await getChatList(localStorage.token)); 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> </script>
<div <div
...@@ -100,7 +106,7 @@ ...@@ -100,7 +106,7 @@
</div> </div>
{#if $user?.role === 'admin'} {#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 <button
class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition" class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
on:click={async () => { on:click={async () => {
...@@ -129,255 +135,326 @@ ...@@ -129,255 +135,326 @@
</div> </div>
</button> </button>
</div> </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 <div class="px-2.5 flex justify-center mb-1">
class="w-full rounded-r py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none" <button
placeholder="Search" class="flex-grow flex space-x-3 rounded-md px-3 py-2 hover:bg-gray-900 transition"
bind:value={search} 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"> <div class="flex self-center">
<svg <div class=" self-center font-medium text-sm">Prompts</div>
xmlns="http://www.w3.org/2000/svg" </div>
fill="none" </button>
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> {/if}
<div class="pl-2.5 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto"> <div class="relative flex flex-col flex-1 overflow-y-auto">
{#each $chats.filter((chat) => { {#if !($settings.saveChatHistory ?? true)}
if (search === '') { <div class="absolute z-40 w-full h-full bg-black/90 flex justify-center">
return true; <div class=" text-left px-5 py-2">
} else { <div class=" font-medium">Chat History is off for this browser.</div>
let title = chat.title.toLowerCase(); <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)) { <div class="mt-3">
return true; <button
} else { 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"
return false; type="button"
} on:click={() => {
} saveSettings({
}) as chat, i} saveChatHistory: true
<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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 16 16"
viewBox="0 0 24 24" fill="currentColor"
stroke-width="1.5" class="w-3 h-3"
stroke="currentColor"
class="w-4 h-4"
> >
<path <path
stroke-linecap="round" fill-rule="evenodd"
stroke-linejoin="round" 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"
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" clip-rule="evenodd"
/> />
</svg> </svg>
</div>
<div <div>Enable Chat History</div>
class=" text-left self-center overflow-hidden {chat.id === $chatId </button>
? 'w-[120px]'
: 'w-[180px]'} "
>
{#if chatTitleEditId === chat.id}
<input bind:value={chatTitle} class=" bg-transparent w-full" />
{:else}
{chat.title}
{/if}
</div>
</div> </div>
</button> </div>
</div>
{/if}
{#if chat.id === $chatId} <div class="px-2.5 mt-1 mb-2 flex justify-center space-x-2">
<div class=" absolute right-[22px] top-[10px]"> <div class="flex w-full" id="chat-search">
{#if chatTitleEditId === chat.id} <div class="self-center pl-3 py-2 rounded-l bg-gray-950">
<div class="flex self-center space-x-1.5"> <svg
<button xmlns="http://www.w3.org/2000/svg"
class=" self-center hover:text-white transition" viewBox="0 0 20 20"
on:click={() => { fill="currentColor"
editChatTitle(chat.id, chatTitle); class="w-4 h-4"
chatTitleEditId = null; >
chatTitle = ''; <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 <path
xmlns="http://www.w3.org/2000/svg" stroke-linecap="round"
viewBox="0 0 20 20" stroke-linejoin="round"
fill="currentColor" 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"
class="w-4 h-4" />
</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 <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
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" viewBox="0 0 20 20"
clip-rule="evenodd" fill="currentColor"
/> class="w-4 h-4"
</svg> >
</button> <path
<button fill-rule="evenodd"
class=" self-center hover:text-white transition" 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"
on:click={() => { clip-rule="evenodd"
chatTitleEditId = null; />
chatTitle = ''; </svg>
}} </button>
> <button
<svg class=" self-center hover:text-white transition"
xmlns="http://www.w3.org/2000/svg" on:click={() => {
viewBox="0 0 20 20" chatTitleEditId = null;
fill="currentColor" chatTitle = '';
class="w-4 h-4" }}
> >
<path <svg
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" xmlns="http://www.w3.org/2000/svg"
/> viewBox="0 0 20 20"
</svg> fill="currentColor"
</button> class="w-4 h-4"
</div> >
{:else if chatDeleteId === chat.id} <path
<div class="flex self-center space-x-1.5"> 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"
<button />
class=" self-center hover:text-white transition" </svg>
on:click={() => { </button>
deleteChat(chat.id); </div>
}} {:else if chatDeleteId === chat.id}
> <div class="flex self-center space-x-1.5">
<svg <button
xmlns="http://www.w3.org/2000/svg" class=" self-center hover:text-white transition"
viewBox="0 0 20 20" on:click={() => {
fill="currentColor" deleteChat(chat.id);
class="w-4 h-4" }}
> >
<path <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
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" viewBox="0 0 20 20"
clip-rule="evenodd" fill="currentColor"
/> class="w-4 h-4"
</svg> >
</button> <path
<button fill-rule="evenodd"
class=" self-center hover:text-white transition" 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"
on:click={() => { clip-rule="evenodd"
chatDeleteId = null; />
}} </svg>
> </button>
<svg <button
xmlns="http://www.w3.org/2000/svg" class=" self-center hover:text-white transition"
viewBox="0 0 20 20" on:click={() => {
fill="currentColor" chatDeleteId = null;
class="w-4 h-4" }}
> >
<path <svg
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" xmlns="http://www.w3.org/2000/svg"
/> viewBox="0 0 20 20"
</svg> fill="currentColor"
</button> class="w-4 h-4"
</div> >
{:else} <path
<div class="flex self-center space-x-1.5"> 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"
<button />
id="delete-chat-button" </svg>
class=" hidden" </button>
on:click={() => { </div>
deleteChat(chat.id); {:else}
}} <div class="flex self-center space-x-1.5">
/> <button
<button id="delete-chat-button"
class=" self-center hover:text-white transition" class=" hidden"
on:click={() => { on:click={() => {
chatTitle = chat.title; deleteChat(chat.id);
chatTitleEditId = chat.id; }}
// editChatTitle(chat.id, 'a'); />
}} <button
> class=" self-center hover:text-white transition"
<svg on:click={() => {
xmlns="http://www.w3.org/2000/svg" chatTitle = chat.title;
fill="none" chatTitleEditId = chat.id;
viewBox="0 0 24 24" // editChatTitle(chat.id, 'a');
stroke-width="1.5" }}
stroke="currentColor"
class="w-4 h-4"
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" fill="none"
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" viewBox="0 0 24 24"
/> stroke-width="1.5"
</svg> stroke="currentColor"
</button> class="w-4 h-4"
<button >
class=" self-center hover:text-white transition" <path
on:click={() => { stroke-linecap="round"
chatDeleteId = chat.id; 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 </svg>
xmlns="http://www.w3.org/2000/svg" </button>
fill="none" <button
viewBox="0 0 24 24" class=" self-center hover:text-white transition"
stroke-width="1.5" on:click={() => {
stroke="currentColor" chatDeleteId = chat.id;
class="w-4 h-4" }}
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" fill="none"
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" viewBox="0 0 24 24"
/> stroke-width="1.5"
</svg> stroke="currentColor"
</button> class="w-4 h-4"
</div> >
{/if} <path
</div> stroke-linecap="round"
{/if} stroke-linejoin="round"
</div> 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"
{/each} />
</svg>
</button>
</div>
{/if}
</div>
{/if}
</div>
{/each}
</div>
</div> </div>
<div class="px-2.5"> <div class="px-2.5">
......
import { dev, browser } from '$app/environment'; import { dev } 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;
export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; 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'; export const WEB_UI_VERSION = 'v1.0.0-alpha-static';
......
...@@ -6,10 +6,13 @@ export const user = writable(undefined); ...@@ -6,10 +6,13 @@ export const user = writable(undefined);
// Frontend // Frontend
export const theme = writable('dark'); export const theme = writable('dark');
export const chatId = writable(''); export const chatId = writable('');
export const chats = writable([]); export const chats = writable([]);
export const models = writable([]); export const models = writable([]);
export const modelfiles = writable([]); export const modelfiles = writable([]);
export const prompts = writable([]);
export const settings = writable({}); export const settings = writable({});
export const showSettings = writable(false); export const showSettings = writable(false);
...@@ -21,7 +21,7 @@ export const splitStream = (splitOn) => { ...@@ -21,7 +21,7 @@ export const splitStream = (splitOn) => {
}; };
export const convertMessagesToHistory = (messages) => { export const convertMessagesToHistory = (messages) => {
let history = { const history = {
messages: {}, messages: {},
currentId: null currentId: null
}; };
...@@ -114,7 +114,7 @@ export const checkVersion = (required, current) => { ...@@ -114,7 +114,7 @@ export const checkVersion = (required, current) => {
export const findWordIndices = (text) => { export const findWordIndices = (text) => {
const regex = /\[([^\]]+)\]/g; const regex = /\[([^\]]+)\]/g;
let matches = []; const matches = [];
let match; let match;
while ((match = regex.exec(text)) !== null) { while ((match = regex.exec(text)) !== null) {
......
...@@ -9,11 +9,12 @@ ...@@ -9,11 +9,12 @@
import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama'; import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
import { getModelfiles } from '$lib/apis/modelfiles'; import { getModelfiles } from '$lib/apis/modelfiles';
import { getPrompts } from '$lib/apis/prompts';
import { getOpenAIModels } from '$lib/apis/openai'; import { getOpenAIModels } from '$lib/apis/openai';
import { user, showSettings, settings, models, modelfiles } from '$lib/stores'; import { user, showSettings, settings, models, modelfiles, prompts } from '$lib/stores';
import { OLLAMA_API_BASE_URL, REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
import Sidebar from '$lib/components/layout/Sidebar.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte';
...@@ -31,36 +32,28 @@ ...@@ -31,36 +32,28 @@
const getModels = async () => { const getModels = async () => {
let models = []; let models = [];
models.push( models.push(
...(await getOllamaModels( ...(await getOllamaModels(localStorage.token).catch((error) => {
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
localStorage.token
).catch((error) => {
toast.error(error); toast.error(error);
return []; 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; return models;
}; };
const setOllamaVersion = async (version: string = '') => { const setOllamaVersion = async (version: string = '') => {
if (version === '') { if (version === '') {
version = await getOllamaVersion( version = await getOllamaVersion(localStorage.token).catch((error) => {
$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL,
localStorage.token
).catch((error) => {
return ''; return '';
}); });
} }
...@@ -101,6 +94,9 @@ ...@@ -101,6 +94,9 @@
console.log(); console.log();
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
await modelfiles.set(await getModelfiles(localStorage.token)); await modelfiles.set(await getModelfiles(localStorage.token));
await prompts.set(await getPrompts(localStorage.token));
console.log($modelfiles); console.log($modelfiles);
modelfiles.subscribe(async () => { 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