Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
ac294a74
Unverified
Commit
ac294a74
authored
Mar 24, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
Mar 24, 2024
Browse files
Merge pull request #1277 from open-webui/dev
0.1.115
parents
2fa94956
4c959891
Changes
53
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
2490 additions
and
5212 deletions
+2490
-5212
backend/requirements.txt
backend/requirements.txt
+1
-0
package-lock.json
package-lock.json
+1870
-5033
package.json
package.json
+1
-1
src/lib/apis/images/index.ts
src/lib/apis/images/index.ts
+5
-5
src/lib/apis/ollama/index.ts
src/lib/apis/ollama/index.ts
+68
-1
src/lib/components/chat/Messages/Placeholder.svelte
src/lib/components/chat/Messages/Placeholder.svelte
+2
-2
src/lib/components/chat/ModelSelector.svelte
src/lib/components/chat/ModelSelector.svelte
+19
-24
src/lib/components/chat/Settings/Chats.svelte
src/lib/components/chat/Settings/Chats.svelte
+0
-33
src/lib/components/chat/Settings/Images.svelte
src/lib/components/chat/Settings/Images.svelte
+81
-18
src/lib/components/chat/Settings/Models.svelte
src/lib/components/chat/Settings/Models.svelte
+144
-44
src/lib/components/common/Dropdown.svelte
src/lib/components/common/Dropdown.svelte
+3
-0
src/lib/components/common/ImagePreview.svelte
src/lib/components/common/ImagePreview.svelte
+17
-4
src/lib/components/common/Modal.svelte
src/lib/components/common/Modal.svelte
+4
-2
src/lib/components/common/Select.svelte
src/lib/components/common/Select.svelte
+87
-0
src/lib/components/documents/Settings/General.svelte
src/lib/components/documents/Settings/General.svelte
+99
-1
src/lib/components/icons/Check.svelte
src/lib/components/icons/Check.svelte
+15
-0
src/lib/components/icons/ChevronDown.svelte
src/lib/components/icons/ChevronDown.svelte
+15
-0
src/lib/components/icons/Search.svelte
src/lib/components/icons/Search.svelte
+19
-0
src/lib/components/layout/Navbar.svelte
src/lib/components/layout/Navbar.svelte
+2
-5
src/lib/components/layout/Sidebar.svelte
src/lib/components/layout/Sidebar.svelte
+38
-39
No files found.
backend/requirements.txt
View file @
ac294a74
...
@@ -45,3 +45,4 @@ PyJWT
...
@@ -45,3 +45,4 @@ PyJWT
pyjwt[crypto]
pyjwt[crypto]
black
black
langfuse
package-lock.json
View file @
ac294a74
This diff is collapsed.
Click to expand it.
package.json
View file @
ac294a74
{
{
"name"
:
"open-webui"
,
"name"
:
"open-webui"
,
"version"
:
"0.1.11
4
"
,
"version"
:
"0.1.11
5
"
,
"private"
:
true
,
"private"
:
true
,
"scripts"
:
{
"scripts"
:
{
"dev"
:
"vite dev --host"
,
"dev"
:
"vite dev --host"
,
...
...
src/lib/apis/images/index.ts
View file @
ac294a74
...
@@ -139,7 +139,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
...
@@ -139,7 +139,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
return
res
.
OPENAI_API_KEY
;
return
res
.
OPENAI_API_KEY
;
};
};
export
const
get
AUTOMATIC1111
Url
=
async
(
token
:
string
=
''
)
=>
{
export
const
get
ImageGenerationEngine
Url
s
=
async
(
token
:
string
=
''
)
=>
{
let
error
=
null
;
let
error
=
null
;
const
res
=
await
fetch
(
`
${
IMAGES_API_BASE_URL
}
/url`
,
{
const
res
=
await
fetch
(
`
${
IMAGES_API_BASE_URL
}
/url`
,
{
...
@@ -168,10 +168,10 @@ export const getAUTOMATIC1111Url = async (token: string = '') => {
...
@@ -168,10 +168,10 @@ export const getAUTOMATIC1111Url = async (token: string = '') => {
throw
error
;
throw
error
;
}
}
return
res
.
AUTOMATIC1111_BASE_URL
;
return
res
;
};
};
export
const
update
AUTOMATIC1111
Url
=
async
(
token
:
string
=
''
,
url
:
string
)
=>
{
export
const
update
ImageGenerationEngine
Url
s
=
async
(
token
:
string
=
''
,
url
s
:
object
=
{}
)
=>
{
let
error
=
null
;
let
error
=
null
;
const
res
=
await
fetch
(
`
${
IMAGES_API_BASE_URL
}
/url/update`
,
{
const
res
=
await
fetch
(
`
${
IMAGES_API_BASE_URL
}
/url/update`
,
{
...
@@ -182,7 +182,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
...
@@ -182,7 +182,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
...(
token
&&
{
authorization
:
`Bearer
${
token
}
`
})
...(
token
&&
{
authorization
:
`Bearer
${
token
}
`
})
},
},
body
:
JSON
.
stringify
({
body
:
JSON
.
stringify
({
url
:
url
...
urls
})
})
})
})
.
then
(
async
(
res
)
=>
{
.
then
(
async
(
res
)
=>
{
...
@@ -203,7 +203,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
...
@@ -203,7 +203,7 @@ export const updateAUTOMATIC1111Url = async (token: string = '', url: string) =>
throw
error
;
throw
error
;
}
}
return
res
.
AUTOMATIC1111_BASE_URL
;
return
res
;
};
};
export
const
getImageSize
=
async
(
token
:
string
=
''
)
=>
{
export
const
getImageSize
=
async
(
token
:
string
=
''
)
=>
{
...
...
src/lib/apis/ollama/index.ts
View file @
ac294a74
...
@@ -271,7 +271,7 @@ export const generateChatCompletion = async (token: string = '', body: object) =
...
@@ -271,7 +271,7 @@ export const generateChatCompletion = async (token: string = '', body: object) =
return
[
res
,
controller
];
return
[
res
,
controller
];
};
};
export
const
cancel
ChatCompletion
=
async
(
token
:
string
=
''
,
requestId
:
string
)
=>
{
export
const
cancel
OllamaRequest
=
async
(
token
:
string
=
''
,
requestId
:
string
)
=>
{
let
error
=
null
;
let
error
=
null
;
const
res
=
await
fetch
(
`
${
OLLAMA_API_BASE_URL
}
/cancel/
${
requestId
}
`
,
{
const
res
=
await
fetch
(
`
${
OLLAMA_API_BASE_URL
}
/cancel/
${
requestId
}
`
,
{
...
@@ -390,6 +390,73 @@ export const pullModel = async (token: string, tagName: string, urlIdx: string |
...
@@ -390,6 +390,73 @@ export const pullModel = async (token: string, tagName: string, urlIdx: string |
return
res
;
return
res
;
};
};
export
const
downloadModel
=
async
(
token
:
string
,
download_url
:
string
,
urlIdx
:
string
|
null
=
null
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
OLLAMA_API_BASE_URL
}
/models/download
${
urlIdx
!==
null
?
`/
${
urlIdx
}
`
:
''
}
`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
Authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
url
:
download_url
})
}
).
catch
((
err
)
=>
{
console
.
log
(
err
);
error
=
err
;
if
(
'
detail
'
in
err
)
{
error
=
err
.
detail
;
}
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
uploadModel
=
async
(
token
:
string
,
file
:
File
,
urlIdx
:
string
|
null
=
null
)
=>
{
let
error
=
null
;
const
formData
=
new
FormData
();
formData
.
append
(
'
file
'
,
file
);
const
res
=
await
fetch
(
`
${
OLLAMA_API_BASE_URL
}
/models/upload
${
urlIdx
!==
null
?
`/
${
urlIdx
}
`
:
''
}
`
,
{
method
:
'
POST
'
,
headers
:
{
Authorization
:
`Bearer
${
token
}
`
},
body
:
formData
}
).
catch
((
err
)
=>
{
console
.
log
(
err
);
error
=
err
;
if
(
'
detail
'
in
err
)
{
error
=
err
.
detail
;
}
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
// export const pullModel = async (token: string, tagName: string) => {
// export const pullModel = async (token: string, tagName: string) => {
// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
// method: 'POST',
// method: 'POST',
...
...
src/lib/components/chat/Messages/Placeholder.svelte
View file @
ac294a74
...
@@ -33,7 +33,7 @@
...
@@ -33,7 +33,7 @@
<img
<img
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
alt="modelfile"
alt="modelfile"
class="
w
-1
4
rounded-full border-[1px] border-gray-200 dark:border-none"
class="
size
-1
2
rounded-full border-[1px] border-gray-200 dark:border-none"
draggable="false"
draggable="false"
/>
/>
{:else}
{:else}
...
@@ -41,7 +41,7 @@
...
@@ -41,7 +41,7 @@
src={models.length === 1
src={models.length === 1
? `${WEBUI_BASE_URL}/static/favicon.png`
? `${WEBUI_BASE_URL}/static/favicon.png`
: `${WEBUI_BASE_URL}/static/favicon.png`}
: `${WEBUI_BASE_URL}/static/favicon.png`}
class="
w
-1
4
rounded-full border-[1px] border-gray-200 dark:border-none"
class="
size
-1
2
rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
alt="logo"
draggable="false"
draggable="false"
/>
/>
...
...
src/lib/components/chat/ModelSelector.svelte
View file @
ac294a74
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
import { models, showSettings, settings, user } from '$lib/stores';
import { models, showSettings, settings, user } from '$lib/stores';
import { onMount, tick, getContext } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import Select from '../common/Select.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
...
@@ -32,30 +33,24 @@
...
@@ -32,30 +33,24 @@
}
}
</script>
</script>
<div class="flex flex-col my-2">
<div class="flex flex-col my-2
w-full
">
{#each selectedModels as selectedModel, selectedModelIdx}
{#each selectedModels as selectedModel, selectedModelIdx}
<div class="flex">
<div class="flex w-full">
<select
<div class="overflow-hidden w-full">
id="models"
<div class="mr-2 max-w-full">
class="outline-none bg-transparent text-lg font-semibold rounded-lg block w-full placeholder-gray-400"
<Select
placeholder={$i18n.t('Select a model')}
items={$models
.filter((model) => model.name !== 'hr')
.map((model) => ({
value: model.id,
label:
model.name + `${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`
}))}
bind:value={selectedModel}
bind:value={selectedModel}
{disabled}
/>
>
</div>
<option class=" text-gray-700" value="" selected disabled
</div>
>{$i18n.t('Select a model')}</option
>
{#each $models as model}
{#if model.name === 'hr'}
<hr />
{:else}
<option value={model.id} class="text-gray-700 text-lg"
>{model.name +
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
>
{/if}
{/each}
</select>
{#if selectedModelIdx === 0}
{#if selectedModelIdx === 0}
<button
<button
...
@@ -136,6 +131,6 @@
...
@@ -136,6 +131,6 @@
{/each}
{/each}
</div>
</div>
<div class="text-left mt-1.5 text-xs text-gray-500">
<div class="text-left mt-1.5
ml-1
text-xs text-gray-500">
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div>
</div>
src/lib/components/chat/Settings/Chats.svelte
View file @
ac294a74
...
@@ -2,7 +2,6 @@
...
@@ -2,7 +2,6 @@
import fileSaver from 'file-saver';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
const { saveAs } = fileSaver;
import { resetVectorDB } from '$lib/apis/rag';
import { chats, user } from '$lib/stores';
import { chats, user } from '$lib/stores';
import {
import {
...
@@ -330,38 +329,6 @@
...
@@ -330,38 +329,6 @@
{$i18n.t('Export All Chats (All Users)')}
{$i18n.t('Export All Chats (All Users)')}
</div>
</div>
</button>
</button>
<hr class=" dark:border-gray-700" />
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
const res = resetVectorDB(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
</button>
{/if}
{/if}
</div>
</div>
</div>
</div>
src/lib/components/chat/Settings/Images.svelte
View file @
ac294a74
...
@@ -4,14 +4,14 @@
...
@@ -4,14 +4,14 @@
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { config, user } from '$lib/stores';
import { config, user } from '$lib/stores';
import {
import {
getAUTOMATIC1111Url,
getImageGenerationModels,
getImageGenerationModels,
getDefaultImageGenerationModel,
getDefaultImageGenerationModel,
updateDefaultImageGenerationModel,
updateDefaultImageGenerationModel,
getImageSize,
getImageSize,
getImageGenerationConfig,
getImageGenerationConfig,
updateImageGenerationConfig,
updateImageGenerationConfig,
updateAUTOMATIC1111Url,
getImageGenerationEngineUrls,
updateImageGenerationEngineUrls,
updateImageSize,
updateImageSize,
getImageSteps,
getImageSteps,
updateImageSteps,
updateImageSteps,
...
@@ -31,6 +31,8 @@
...
@@ -31,6 +31,8 @@
let enableImageGeneration = false;
let enableImageGeneration = false;
let AUTOMATIC1111_BASE_URL = '';
let AUTOMATIC1111_BASE_URL = '';
let COMFYUI_BASE_URL = '';
let OPENAI_API_KEY = '';
let OPENAI_API_KEY = '';
let selectedModel = '';
let selectedModel = '';
...
@@ -49,16 +51,38 @@
...
@@ -49,16 +51,38 @@
});
});
};
};
const updateAUTOMATIC1111UrlHandler = async () => {
const updateUrlHandler = async () => {
const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch(
if (imageGenerationEngine === 'comfyui') {
(error) => {
const res = await updateImageGenerationEngineUrls(localStorage.token, {
COMFYUI_BASE_URL: COMFYUI_BASE_URL
}).catch((error) => {
toast.error(error);
toast.error(error);
console.log(error);
return null;
return null;
});
if (res) {
COMFYUI_BASE_URL = res.COMFYUI_BASE_URL;
await getModels();
if (models) {
toast.success($i18n.t('Server connection verified'));
}
} else {
({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
}
}
);
} else {
const res = await updateImageGenerationEngineUrls(localStorage.token, {
AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL
}).catch((error) => {
toast.error(error);
return null;
});
if (res) {
if (res) {
AUTOMATIC1111_BASE_URL = res;
AUTOMATIC1111_BASE_URL = res
.AUTOMATIC1111_BASE_URL
;
await getModels();
await getModels();
...
@@ -66,7 +90,8 @@
...
@@ -66,7 +90,8 @@
toast.success($i18n.t('Server connection verified'));
toast.success($i18n.t('Server connection verified'));
}
}
} else {
} else {
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
({ AUTOMATIC1111_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
}
}
}
};
};
const updateImageGeneration = async () => {
const updateImageGeneration = async () => {
...
@@ -101,7 +126,11 @@
...
@@ -101,7 +126,11 @@
imageGenerationEngine = res.engine;
imageGenerationEngine = res.engine;
enableImageGeneration = res.enabled;
enableImageGeneration = res.enabled;
}
}
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
const URLS = await getImageGenerationEngineUrls(localStorage.token);
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
imageSize = await getImageSize(localStorage.token);
imageSize = await getImageSize(localStorage.token);
...
@@ -154,6 +183,7 @@
...
@@ -154,6 +183,7 @@
}}
}}
>
>
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
</select>
</select>
</div>
</div>
...
@@ -171,6 +201,9 @@
...
@@ -171,6 +201,9 @@
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
enableImageGeneration = false;
enableImageGeneration = false;
} else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') {
toast.error($i18n.t('ComfyUI Base URL is required.'));
enableImageGeneration = false;
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
toast.error($i18n.t('OpenAI API Key is required.'));
toast.error($i18n.t('OpenAI API Key is required.'));
enableImageGeneration = false;
enableImageGeneration = false;
...
@@ -204,12 +237,10 @@
...
@@ -204,12 +237,10 @@
/>
/>
</div>
</div>
<button
<button
class="px-
3
bg-gray-
2
00 hover:bg-gray-
3
00 dark:bg-gray-
60
0 dark:hover:bg-gray-
7
00 rounded-lg transition"
class="px-
2.5
bg-gray-
1
00 hover:bg-gray-
200 text-gray-8
00 dark:bg-gray-
85
0 dark:hover:bg-gray-
800 dark:text-gray-1
00 rounded-lg transition"
type="button"
type="button"
on:click={() => {
on:click={() => {
// updateOllamaAPIUrlHandler();
updateUrlHandler();
updateAUTOMATIC1111UrlHandler();
}}
}}
>
>
<svg
<svg
...
@@ -237,6 +268,37 @@
...
@@ -237,6 +268,37 @@
{$i18n.t('(e.g. `sh webui.sh --api`)')}
{$i18n.t('(e.g. `sh webui.sh --api`)')}
</a>
</a>
</div>
</div>
{:else if imageGenerationEngine === 'comfyui'}
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={COMFYUI_BASE_URL}
/>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
type="button"
on:click={() => {
updateUrlHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{:else if imageGenerationEngine === 'openai'}
{:else if imageGenerationEngine === 'openai'}
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div>
<div class="flex w-full">
<div class="flex w-full">
...
@@ -261,6 +323,7 @@
...
@@ -261,6 +323,7 @@
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedModel}
bind:value={selectedModel}
placeholder={$i18n.t('Select a model')}
placeholder={$i18n.t('Select a model')}
required
>
>
{#if !selectedModel}
{#if !selectedModel}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
...
...
src/lib/components/chat/Settings/Models.svelte
View file @
ac294a74
...
@@ -5,9 +5,12 @@
...
@@ -5,9 +5,12 @@
import {
import {
createModel,
createModel,
deleteModel,
deleteModel,
downloadModel,
getOllamaUrls,
getOllamaUrls,
getOllamaVersion,
getOllamaVersion,
pullModel
pullModel,
cancelOllamaRequest,
uploadModel
} from '$lib/apis/ollama';
} from '$lib/apis/ollama';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, models, user } from '$lib/stores';
import { WEBUI_NAME, models, user } from '$lib/stores';
...
@@ -60,11 +63,13 @@
...
@@ -60,11 +63,13 @@
let pullProgress = null;
let pullProgress = null;
let modelUploadMode = 'file';
let modelUploadMode = 'file';
let modelInputFile
= ''
;
let modelInputFile
: File[] | null = null
;
let modelFileUrl = '';
let modelFileUrl = '';
let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
let modelFileDigest = '';
let modelFileDigest = '';
let uploadProgress = null;
let uploadProgress = null;
let uploadMessage = '';
let deleteModelTag = '';
let deleteModelTag = '';
...
@@ -159,7 +164,7 @@
...
@@ -159,7 +164,7 @@
// Remove the downloaded model
// Remove the downloaded model
delete modelDownloadStatus[modelName];
delete modelDownloadStatus[modelName];
console.log(data)
;
modelDownloadStatus = { ...modelDownloadStatus }
;
if (!data.success) {
if (!data.success) {
toast.error(data.error);
toast.error(data.error);
...
@@ -184,35 +189,32 @@
...
@@ -184,35 +189,32 @@
const uploadModelHandler = async () => {
const uploadModelHandler = async () => {
modelTransferring = true;
modelTransferring = true;
uploadProgress = 0;
let uploaded = false;
let uploaded = false;
let fileResponse = null;
let fileResponse = null;
let name = '';
let name = '';
if (modelUploadMode === 'file') {
if (modelUploadMode === 'file') {
const file = modelInputFile[0];
const file = modelInputFile ? modelInputFile[0] : null;
const formData = new FormData();
formData.append('file', file);
if (file) {
uploadMessage = 'Uploading...';
fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, {
method: 'POST',
fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch(
headers: {
(error) => {
...($user && { Authorization: `Bearer ${localStorage.token}` })
toast.error(error);
},
body: formData
}).catch((error) => {
console.log(error);
return null;
return null;
});
} else {
fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, {
method: 'GET',
headers: {
...($user && { Authorization: `Bearer ${localStorage.token}` })
}
}
}).catch((error) => {
);
console.log(error);
}
} else {
uploadProgress = 0;
fileResponse = await downloadModel(
localStorage.token,
modelFileUrl,
selectedOllamaUrlIdx
).catch((error) => {
toast.error(error);
return null;
return null;
});
});
}
}
...
@@ -235,6 +237,9 @@
...
@@ -235,6 +237,9 @@
let data = JSON.parse(line.replace(/^data: /, ''));
let data = JSON.parse(line.replace(/^data: /, ''));
if (data.progress) {
if (data.progress) {
if (uploadMessage) {
uploadMessage = '';
}
uploadProgress = data.progress;
uploadProgress = data.progress;
}
}
...
@@ -318,7 +323,11 @@
...
@@ -318,7 +323,11 @@
}
}
modelFileUrl = '';
modelFileUrl = '';
modelInputFile = '';
if (modelUploadInputElement) {
modelUploadInputElement.value = '';
}
modelInputFile = null;
modelTransferring = false;
modelTransferring = false;
uploadProgress = null;
uploadProgress = null;
...
@@ -364,12 +373,24 @@
...
@@ -364,12 +373,24 @@
for (const line of lines) {
for (const line of lines) {
if (line !== '') {
if (line !== '') {
let data = JSON.parse(line);
let data = JSON.parse(line);
console.log(data);
if (data.error) {
if (data.error) {
throw data.error;
throw data.error;
}
}
if (data.detail) {
if (data.detail) {
throw data.detail;
throw data.detail;
}
}
if (data.id) {
modelDownloadStatus[opts.modelName] = {
...modelDownloadStatus[opts.modelName],
requestId: data.id,
reader,
done: false
};
console.log(data);
}
if (data.status) {
if (data.status) {
if (data.digest) {
if (data.digest) {
let downloadProgress = 0;
let downloadProgress = 0;
...
@@ -379,11 +400,17 @@
...
@@ -379,11 +400,17 @@
downloadProgress = 100;
downloadProgress = 100;
}
}
modelDownloadStatus[opts.modelName] = {
modelDownloadStatus[opts.modelName] = {
...modelDownloadStatus[opts.modelName],
pullProgress: downloadProgress,
pullProgress: downloadProgress,
digest: data.digest
digest: data.digest
};
};
} else {
} else {
toast.success(data.status);
toast.success(data.status);
modelDownloadStatus[opts.modelName] = {
...modelDownloadStatus[opts.modelName],
done: data.status === 'success'
};
}
}
}
}
}
}
...
@@ -396,7 +423,14 @@
...
@@ -396,7 +423,14 @@
opts.callback({ success: false, error, modelName: opts.modelName });
opts.callback({ success: false, error, modelName: opts.modelName });
}
}
}
}
console.log(modelDownloadStatus[opts.modelName]);
if (modelDownloadStatus[opts.modelName].done) {
opts.callback({ success: true, modelName: opts.modelName });
opts.callback({ success: true, modelName: opts.modelName });
} else {
opts.callback({ success: false, error: 'Download canceled', modelName: opts.modelName });
}
}
}
};
};
...
@@ -466,6 +500,18 @@
...
@@ -466,6 +500,18 @@
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
});
});
const cancelModelPullHandler = async (model: string) => {
const { reader, requestId } = modelDownloadStatus[model];
if (reader) {
await reader.cancel();
await cancelOllamaRequest(localStorage.token, requestId);
delete modelDownloadStatus[model];
await deleteModel(localStorage.token, model);
toast.success(`${model} download has been canceled`);
}
};
</script>
</script>
<div class="flex flex-col h-full justify-between text-sm">
<div class="flex flex-col h-full justify-between text-sm">
...
@@ -596,20 +642,58 @@
...
@@ -596,20 +642,58 @@
{#if Object.keys(modelDownloadStatus).length > 0}
{#if Object.keys(modelDownloadStatus).length > 0}
{#each Object.keys(modelDownloadStatus) as model}
{#each Object.keys(modelDownloadStatus) as model}
{#if 'pullProgress' in modelDownloadStatus[model]}
<div class="flex flex-col">
<div class="flex flex-col">
<div class="font-medium mb-1">{model}</div>
<div class="font-medium mb-1">{model}</div>
<div class="">
<div class="">
<div class="flex flex-row justify-between space-x-4 pr-2">
<div class=" flex-1">
<div
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
style="width: {Math.max(
15,
modelDownloadStatus[model].pullProgress ?? 0
)}%"
>
>
{modelDownloadStatus[model].pullProgress ?? 0}%
{modelDownloadStatus[model].pullProgress ?? 0}%
</div>
</div>
</div>
<Tooltip content="Cancel">
<button
class="text-gray-800 dark:text-gray-100"
on:click={() => {
cancelModelPullHandler(model);
}}
>
<svg
class="w-4 h-4 text-gray-800 dark:text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</Tooltip>
</div>
{#if 'digest' in modelDownloadStatus[model]}
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelDownloadStatus[model].digest}
{modelDownloadStatus[model].digest}
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
{/if}
{/each}
{/each}
{/if}
{/if}
</div>
</div>
...
@@ -715,7 +799,7 @@
...
@@ -715,7 +799,7 @@
<button
<button
type="button"
type="button"
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
class="w-full rounded-lg text-left py-2 px-4
bg-white
dark:text-gray-300 dark:bg-gray-850"
on:click={() => {
on:click={() => {
modelUploadInputElement.click();
modelUploadInputElement.click();
}}
}}
...
@@ -730,7 +814,7 @@
...
@@ -730,7 +814,7 @@
{:else}
{:else}
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<input
<input
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
class="w-full rounded-lg text-left py-2 px-4
bg-white
dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
''
''
? 'mr-2'
? 'mr-2'
: ''}"
: ''}"
...
@@ -745,7 +829,7 @@
...
@@ -745,7 +829,7 @@
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<button
<button
class="px-
3
text-gray-
1
00
bg-emerald-600 hover:bg-emerald-700 disabled:bg
-gray-
7
00 disabled:cursor-not-allowed
rounded
transition"
class="px-
2.5 bg-gray-100 hover:bg-gray-200
text-gray-
8
00
dark:bg-gray-850 dark:hover:bg-gray-800 dark:text
-gray-
1
00
rounded-lg
disabled:cursor-not-allowed transition"
type="submit"
type="submit"
disabled={modelTransferring}
disabled={modelTransferring}
>
>
...
@@ -800,7 +884,7 @@
...
@@ -800,7 +884,7 @@
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
<textarea
<textarea
bind:value={modelFileContent}
bind:value={modelFileContent}
class="w-full rounded py-2 px-4 text-sm dark:text-gray-
3
00 dark:bg-gray-8
0
0 outline-none resize-none"
class="w-full rounded
-lg
py-2 px-4 text-sm
bg-gray-100
dark:text-gray-
1
00 dark:bg-gray-8
5
0 outline-none resize-none"
rows="6"
rows="6"
/>
/>
</div>
</div>
...
@@ -815,7 +899,23 @@
...
@@ -815,7 +899,23 @@
>
>
</div>
</div>
{#if uploadProgress !== null}
{#if uploadMessage}
<div class="mt-2">
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
<div class="w-full rounded-full dark:bg-gray-800">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: 100%"
>
{uploadMessage}
</div>
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelFileDigest}
</div>
</div>
{:else if uploadProgress !== null}
<div class="mt-2">
<div class="mt-2">
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
...
...
src/lib/components/common/Dropdown.svelte
View file @
ac294a74
...
@@ -2,6 +2,8 @@
...
@@ -2,6 +2,8 @@
import { DropdownMenu } from 'bits-ui';
import { DropdownMenu } from 'bits-ui';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { flyAndScale } from '$lib/utils/transitions';
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher();
</script>
</script>
...
@@ -20,6 +22,7 @@
...
@@ -20,6 +22,7 @@
sideOffset={8}
sideOffset={8}
side="bottom"
side="bottom"
align="start"
align="start"
transition={flyAndScale}
>
>
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
<div class="flex items-center">Profile</div>
<div class="flex items-center">Profile</div>
...
...
src/lib/components/common/ImagePreview.svelte
View file @
ac294a74
...
@@ -2,6 +2,22 @@
...
@@ -2,6 +2,22 @@
export let show = false;
export let show = false;
export let src = '';
export let src = '';
export let alt = '';
export let alt = '';
const downloadImage = (url, filename) => {
fetch(url)
.then((response) => response.blob())
.then((blob) => {
const objectUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(objectUrl);
})
.catch((error) => console.error('Error downloading image:', error));
};
</script>
</script>
{#if show}
{#if show}
...
@@ -35,10 +51,7 @@
...
@@ -35,10 +51,7 @@
<button
<button
class=" p-5"
class=" p-5"
on:click={() => {
on:click={() => {
const a = document.createElement('a');
downloadImage(src, 'Image.png');
a.href = src;
a.download = 'Image.png';
a.click();
}}
}}
>
>
<svg
<svg
...
...
src/lib/components/common/Modal.svelte
View file @
ac294a74
...
@@ -2,6 +2,8 @@
...
@@ -2,6 +2,8 @@
import { onMount } from 'svelte';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { fade } from 'svelte/transition';
import { flyAndScale } from '$lib/utils/transitions';
export let show = true;
export let show = true;
export let size = 'md';
export let size = 'md';
...
@@ -41,10 +43,10 @@
...
@@ -41,10 +43,10 @@
}}
}}
>
>
<div
<div
class="
modal-content
m-auto rounded-2xl max-w-full {sizeToWidth(
class=" m-auto rounded-2xl max-w-full {sizeToWidth(
size
size
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
in:f
ade={{ duration: 10 }}
in:f
lyAndScale
on:click={(e) => {
on:click={(e) => {
e.stopPropagation();
e.stopPropagation();
}}
}}
...
...
src/lib/components/common/Select.svelte
0 → 100644
View file @
ac294a74
<script lang="ts">
import { Select } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { createEventDispatcher } from 'svelte';
import ChevronDown from '../icons/ChevronDown.svelte';
import Check from '../icons/Check.svelte';
import Search from '../icons/Search.svelte';
const dispatch = createEventDispatcher();
export let value = '';
export let placeholder = 'Select a model';
export let items = [
{ value: 'mango', label: 'Mango' },
{ value: 'watermelon', label: 'Watermelon' },
{ value: 'apple', label: 'Apple' },
{ value: 'pineapple', label: 'Pineapple' },
{ value: 'orange', label: 'Orange' }
];
let searchValue = '';
$: filteredItems = searchValue
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
: items;
</script>
<Select.Root
{items}
onOpenChange={() => {
searchValue = '';
}}
selected={items.find((item) => item.value === value)}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
}}
>
<Select.Trigger class="relative w-full" aria-label={placeholder}>
<Select.Value
class="inline-flex h-input px-0.5 w-full outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
{placeholder}
/>
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
</Select.Trigger>
<Select.Content
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
transition={flyAndScale}
sideOffset={4}
>
<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3">
<Search className="size-4" strokeWidth="2.5" />
<input
bind:value={searchValue}
class="w-full text-sm bg-transparent outline-none"
placeholder="Search a model"
/>
</div>
<hr class="border-gray-100 dark:border-gray-800" />
<div class="px-3 my-2 max-h-80 overflow-y-auto">
{#each filteredItems as item}
<Select.Item
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 data-[highlighted]:bg-muted"
value={item.value}
label={item.label}
>
{item.label}
{#if value === item.value}
<div class="ml-auto">
<Check />
</div>
{/if}
</Select.Item>
{:else}
<span class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100">
No results found
</span>
{/each}
</div>
</Select.Content>
<Select.Input name="favoriteFruit" />
</Select.Root>
src/lib/components/documents/Settings/General.svelte
View file @
ac294a74
...
@@ -5,8 +5,10 @@
...
@@ -5,8 +5,10 @@
updateRAGConfig,
updateRAGConfig,
getQuerySettings,
getQuerySettings,
scanDocs,
scanDocs,
updateQuerySettings
updateQuerySettings,
resetVectorDB
} from '$lib/apis/rag';
} from '$lib/apis/rag';
import { documents } from '$lib/stores';
import { documents } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
...
@@ -17,6 +19,8 @@
...
@@ -17,6 +19,8 @@
let loading = false;
let loading = false;
let showResetConfirm = false;
let chunkSize = 0;
let chunkSize = 0;
let chunkOverlap = 0;
let chunkOverlap = 0;
let pdfExtractImages = true;
let pdfExtractImages = true;
...
@@ -231,6 +235,100 @@
...
@@ -231,6 +235,100 @@
/>
/>
</div>
</div>
</div>
</div>
<hr class=" dark:border-gray-700" />
{#if showResetConfirm}
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
<div class="flex items-center space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
<span>{$i18n.t('Are you sure?')}</span>
</div>
<div class="flex space-x-1.5 items-center">
<button
class="hover:text-white transition"
on:click={() => {
const res = resetVectorDB(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Success'));
}
showResetConfirm = false;
}}
>
<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="hover:text-white transition"
on:click={() => {
showResetConfirm = false;
}}
>
<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>
</div>
{:else}
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
showResetConfirm = true;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
</button>
{/if}
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<div class="flex justify-end pt-3 text-sm font-medium">
...
...
src/lib/components/icons/Check.svelte
0 → 100644
View file @
ac294a74
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
src/lib/components/icons/ChevronDown.svelte
0 → 100644
View file @
ac294a74
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
src/lib/components/icons/Search.svelte
0 → 100644
View file @
ac294a74
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
/>
</svg>
src/lib/components/layout/Navbar.svelte
View file @
ac294a74
...
@@ -69,10 +69,7 @@
...
@@ -69,10 +69,7 @@
</script>
</script>
<ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} />
<ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} />
<nav
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
id="nav"
class=" sticky py-2.5 top-0 flex flex-row justify-center bg-white/95 dark:bg-gray-900/90 dark:text-gray-200 backdrop-blur-xl z-30"
>
<div
<div
class=" flex {$settings?.fullScreenMode ?? null
class=" flex {$settings?.fullScreenMode ?? null
? 'max-w-full'
? 'max-w-full'
...
@@ -113,7 +110,7 @@
...
@@ -113,7 +110,7 @@
<Tags {tags} {deleteTag} {addTag} />
<Tags {tags} {deleteTag} {addTag} />
<button
<button
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition
border dark:border-gray-600
"
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-lg transition"
on:click={async () => {
on:click={async () => {
showShareChatModal = !showShareChatModal;
showShareChatModal = !showShareChatModal;
...
...
src/lib/components/layout/Sidebar.svelte
View file @
ac294a74
...
@@ -20,10 +20,9 @@
...
@@ -20,10 +20,9 @@
getAllChatTags
getAllChatTags
} from '$lib/apis/chats';
} from '$lib/apis/chats';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import { slide } from 'svelte/transition';
import {
fade,
slide } from 'svelte/transition';
import { WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '../common/Tooltip.svelte';
import Tooltip from '../common/Tooltip.svelte';
import Dropdown from '../common/Dropdown.svelte';
import ChatMenu from './Sidebar/ChatMenu.svelte';
import ChatMenu from './Sidebar/ChatMenu.svelte';
let show = false;
let show = false;
...
@@ -108,7 +107,7 @@
...
@@ -108,7 +107,7 @@
bind:this={navElement}
bind:this={navElement}
class="h-screen max-h-[100dvh] min-h-screen {show
class="h-screen max-h-[100dvh] min-h-screen {show
? 'lg:relative w-[260px]'
? 'lg:relative w-[260px]'
: '-translate-x-[260px] w-[0px]'}
bg-
black
text-gray-
2
00
shadow-2xl
text-sm transition z-40 fixed top-0 left-0
: '-translate-x-[260px] w-[0px]'} bg-
gray-50
text-gray-
9
00
dark:bg-gray-950 dark:text-gray-200
text-sm transition z-40 fixed top-0 left-0
"
"
>
>
<div
<div
...
@@ -119,7 +118,7 @@
...
@@ -119,7 +118,7 @@
<div class="px-2 flex justify-center space-x-2">
<div class="px-2 flex justify-center space-x-2">
<a
<a
id="sidebar-new-chat-button"
id="sidebar-new-chat-button"
class="flex-grow flex justify-between rounded-xl px-
3.5
py-2 hover:bg-gray-900 transition"
class="flex-grow flex justify-between rounded-xl px-
4
py-2
hover:bg-gray-200 dark:
hover:bg-gray-900 transition"
href="/"
href="/"
on:click={async () => {
on:click={async () => {
selectedChatId = null;
selectedChatId = null;
...
@@ -135,7 +134,7 @@
...
@@ -135,7 +134,7 @@
<div class="self-center mr-1.5">
<div class="self-center mr-1.5">
<img
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
src="{WEBUI_BASE_URL}/static/favicon.png"
class="
w-7
-translate-x-1.5 rounded-full"
class="
size-6
-translate-x-1.5 rounded-full"
alt="logo"
alt="logo"
/>
/>
</div>
</div>
...
@@ -164,7 +163,7 @@
...
@@ -164,7 +163,7 @@
{#if $user?.role === 'admin'}
{#if $user?.role === 'admin'}
<div class="px-2 flex justify-center mt-0.5">
<div class="px-2 flex justify-center mt-0.5">
<a
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2
hover:bg-gray-200 dark:
hover:bg-gray-900 transition"
href="/modelfiles"
href="/modelfiles"
on:click={() => {
on:click={() => {
selectedChatId = null;
selectedChatId = null;
...
@@ -176,7 +175,7 @@
...
@@ -176,7 +175,7 @@
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
fill="none"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="
1.5
"
stroke-width="
2
"
stroke="currentColor"
stroke="currentColor"
class="w-4 h-4"
class="w-4 h-4"
>
>
...
@@ -196,7 +195,7 @@
...
@@ -196,7 +195,7 @@
<div class="px-2 flex justify-center">
<div class="px-2 flex justify-center">
<a
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2
hover:bg-gray-200 dark:
hover:bg-gray-900 transition"
href="/prompts"
href="/prompts"
on:click={() => {
on:click={() => {
selectedChatId = null;
selectedChatId = null;
...
@@ -208,7 +207,7 @@
...
@@ -208,7 +207,7 @@
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
fill="none"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="
1.5
"
stroke-width="
2
"
stroke="currentColor"
stroke="currentColor"
class="w-4 h-4"
class="w-4 h-4"
>
>
...
@@ -228,7 +227,7 @@
...
@@ -228,7 +227,7 @@
<div class="px-2 flex justify-center mb-1">
<div class="px-2 flex justify-center mb-1">
<a
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2
hover:bg-gray-200 dark:
hover:bg-gray-900 transition"
href="/documents"
href="/documents"
on:click={() => {
on:click={() => {
selectedChatId = null;
selectedChatId = null;
...
@@ -240,7 +239,7 @@
...
@@ -240,7 +239,7 @@
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
fill="none"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="
1.5
"
stroke-width="
2
"
stroke="currentColor"
stroke="currentColor"
class="w-4 h-4"
class="w-4 h-4"
>
>
...
@@ -261,7 +260,7 @@
...
@@ -261,7 +260,7 @@
<div class="relative flex flex-col flex-1 overflow-y-auto">
<div class="relative flex flex-col flex-1 overflow-y-auto">
{#if !($settings.saveChatHistory ?? true)}
{#if !($settings.saveChatHistory ?? true)}
<div class="absolute z-40 w-full h-full bg-black/90 flex justify-center">
<div class="absolute z-40 w-full h-full
bg-gray-50/90 dark:
bg-black/90 flex justify-center">
<div class=" text-left px-5 py-2">
<div class=" text-left px-5 py-2">
<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
<div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div>
<div class="text-xs mt-2">
<div class="text-xs mt-2">
...
@@ -305,7 +304,7 @@
...
@@ -305,7 +304,7 @@
<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
<div class="px-2 mt-1 mb-2 flex justify-center space-x-2">
<div class="flex w-full" id="chat-search">
<div class="flex w-full" id="chat-search">
<div class="self-center pl-3 py-2 rounded-l-xl bg-gray-950">
<div class="self-center pl-3 py-2 rounded-l-xl
bg-white dark:
bg-gray-950">
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
viewBox="0 0 20 20"
...
@@ -321,7 +320,7 @@
...
@@ -321,7 +320,7 @@
</div>
</div>
<input
<input
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm text-gray-300 bg-gray-950 outline-none"
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm
dark:
text-gray-300
dark:
bg-gray-950 outline-none"
placeholder={$i18n.t('Search')}
placeholder={$i18n.t('Search')}
bind:value={search}
bind:value={search}
on:focus={() => {
on:focus={() => {
...
@@ -334,7 +333,7 @@
...
@@ -334,7 +333,7 @@
{#if $tags.length > 0}
{#if $tags.length > 0}
<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
<div class="px-2.5 mt-0.5 mb-2 flex gap-1 flex-wrap">
<button
<button
class="px-2.5 text-xs font-medium bg-gray-
900
hover:bg-gray-800 transition rounded-full"
class="px-2.5 text-xs font-medium bg-gray-
100 dark:bg-gray-900 dark:
hover:bg-gray-800 transition rounded-full"
on:click={async () => {
on:click={async () => {
await chats.set(await getChatList(localStorage.token));
await chats.set(await getChatList(localStorage.token));
}}
}}
...
@@ -343,7 +342,7 @@
...
@@ -343,7 +342,7 @@
</button>
</button>
{#each $tags as tag}
{#each $tags as tag}
<button
<button
class="px-2.5 text-xs font-medium bg-gray-
900
hover:bg-gray-800 transition rounded-full"
class="px-2.5 text-xs font-medium bg-gray-
100 dark:bg-gray-900 dark:
hover:bg-gray-800 transition rounded-full"
on:click={async () => {
on:click={async () => {
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
if (chatIds.length === 0) {
if (chatIds.length === 0) {
...
@@ -385,10 +384,10 @@
...
@@ -385,10 +384,10 @@
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
chat.id === chatTitleEditId ||
chat.id === chatTitleEditId ||
chat.id === chatDeleteId
chat.id === chatDeleteId
? 'bg-gray-900'
? 'bg-gray-
300 dark:bg-gray-
900'
: chat.id === selectedChatId
: chat.id === selectedChatId
? 'bg-gray-950'
? 'bg-gray-
100 dark:bg-gray-
950'
: 'group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
: 'group-hover:bg-gray-
100 dark:group-hover:bg-gray-
950'} whitespace-nowrap text-ellipsis"
>
>
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
</div>
</div>
...
@@ -397,10 +396,10 @@
...
@@ -397,10 +396,10 @@
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId ||
chat.id === chatTitleEditId ||
chat.id === chatTitleEditId ||
chat.id === chatDeleteId
chat.id === chatDeleteId
? 'bg-gray-900'
? 'bg-gray-
300 dark:bg-gray-
900'
: chat.id === selectedChatId
: chat.id === selectedChatId
? 'bg-gray-950'
? 'bg-gray-
100 dark:bg-gray-
950'
: 'group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
: '
group-hover:bg-gray-100 dark:
group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
href="/c/{chat.id}"
href="/c/{chat.id}"
on:click={() => {
on:click={() => {
selectedChatId = chat.id;
selectedChatId = chat.id;
...
@@ -422,10 +421,10 @@
...
@@ -422,10 +421,10 @@
class="
class="
{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
{chat.id === $chatId || chat.id === chatTitleEditId || chat.id === chatDeleteId
? '
from-gray-900'
? '
from-gray-300 dark:
from-gray-900'
: chat.id === selectedChatId
: chat.id === selectedChatId
? 'from-gray-950'
? 'from-gray-
100 dark:from-gray-
950'
: 'invisible group-hover:visible from-gray-950'}
: 'invisible group-hover:visible
from-gray-100 dark:
from-gray-950'}
absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
absolute right-[10px] top-[10px] pr-2 pl-5 bg-gradient-to-l from-80%
to-transparent"
to-transparent"
...
@@ -433,7 +432,7 @@
...
@@ -433,7 +432,7 @@
{#if chatTitleEditId === chat.id}
{#if chatTitleEditId === chat.id}
<div class="flex self-center space-x-1.5 z-10">
<div class="flex self-center space-x-1.5 z-10">
<button
<button
class=" self-center hover:text-white transition"
class=" self-center
dark:
hover:text-white transition"
on:click={() => {
on:click={() => {
editChatTitle(chat.id, chatTitle);
editChatTitle(chat.id, chatTitle);
chatTitleEditId = null;
chatTitleEditId = null;
...
@@ -454,7 +453,7 @@
...
@@ -454,7 +453,7 @@
</svg>
</svg>
</button>
</button>
<button
<button
class=" self-center hover:text-white transition"
class=" self-center
dark:
hover:text-white transition"
on:click={() => {
on:click={() => {
chatTitleEditId = null;
chatTitleEditId = null;
chatTitle = '';
chatTitle = '';
...
@@ -475,7 +474,7 @@
...
@@ -475,7 +474,7 @@
{:else if chatDeleteId === chat.id}
{:else if chatDeleteId === chat.id}
<div class="flex self-center space-x-1.5 z-10">
<div class="flex self-center space-x-1.5 z-10">
<button
<button
class=" self-center hover:text-white transition"
class=" self-center
dark:
hover:text-white transition"
on:click={() => {
on:click={() => {
deleteChat(chat.id);
deleteChat(chat.id);
}}
}}
...
@@ -494,7 +493,7 @@
...
@@ -494,7 +493,7 @@
</svg>
</svg>
</button>
</button>
<button
<button
class=" self-center hover:text-white transition"
class=" self-center
dark:
hover:text-white transition"
on:click={() => {
on:click={() => {
chatDeleteId = null;
chatDeleteId = null;
}}
}}
...
@@ -527,7 +526,7 @@
...
@@ -527,7 +526,7 @@
>
>
<button
<button
aria-label="Chat Menu"
aria-label="Chat Menu"
class=" self-center hover:text-white transition"
class=" self-center
dark:
hover:text-white transition"
on:click={() => {
on:click={() => {
selectedChatId = chat.id;
selectedChatId = chat.id;
}}
}}
...
@@ -558,7 +557,7 @@
...
@@ -558,7 +557,7 @@
<div class="flex flex-col">
<div class="flex flex-col">
{#if $user !== undefined}
{#if $user !== undefined}
<button
<button
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-900 transition"
class=" flex rounded-xl py-3 px-3.5 w-full
hover:bg-gray-200 dark:
hover:bg-gray-900 transition"
on:click={() => {
on:click={() => {
showDropdown = !showDropdown;
showDropdown = !showDropdown;
}}
}}
...
@@ -576,13 +575,13 @@
...
@@ -576,13 +575,13 @@
{#if showDropdown}
{#if showDropdown}
<div
<div
id="dropdownDots"
id="dropdownDots"
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-gray-900"
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px]
bg-white dark:
bg-gray-900"
in:
slide={{ duration: 1
5
0 }}
transition:fade|
slide={{ duration: 1
0
0 }}
>
>
<div class="py-2 w-full">
<div class="py-2 w-full">
{#if $user.role === 'admin'}
{#if $user.role === 'admin'}
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
class="flex py-2.5 px-3.5 w-full
dark:
hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
goto('/admin');
goto('/admin');
showDropdown = false;
showDropdown = false;
...
@@ -608,7 +607,7 @@
...
@@ -608,7 +607,7 @@
</button>
</button>
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
class="flex py-2.5 px-3.5 w-full
dark:
hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
goto('/playground');
goto('/playground');
showDropdown = false;
showDropdown = false;
...
@@ -635,7 +634,7 @@
...
@@ -635,7 +634,7 @@
{/if}
{/if}
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
class="flex py-2.5 px-3.5 w-full
dark:
hover:bg-gray-800 transition"
on:click={async () => {
on:click={async () => {
await showSettings.set(true);
await showSettings.set(true);
showDropdown = false;
showDropdown = false;
...
@@ -666,11 +665,11 @@
...
@@ -666,11 +665,11 @@
</button>
</button>
</div>
</div>
<hr class=" border-gray-700 m-0 p-0" />
<hr class="
dark:
border-gray-700 m-0 p-0" />
<div class="py-2 w-full">
<div class="py-2 w-full">
<button
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
class="flex py-2.5 px-3.5 w-full
dark:
hover:bg-gray-800 transition"
on:click={() => {
on:click={() => {
localStorage.removeItem('token');
localStorage.removeItem('token');
location.href = '/auth';
location.href = '/auth';
...
@@ -722,7 +721,7 @@
...
@@ -722,7 +721,7 @@
}}
}}
><span class="" data-state="closed"
><span class="" data-state="closed"
><div
><div
class="flex h-[72px] w-8 items-center justify-center opacity-
2
0 group-hover:opacity-100 transition"
class="flex h-[72px] w-8 items-center justify-center opacity-
5
0 group-hover:opacity-100 transition"
>
>
<div class="flex h-6 w-6 flex-col items-center">
<div class="flex h-6 w-6 flex-col items-center">
<div
<div
...
...
Prev
1
2
3
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment