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
e57c0c30
Commit
e57c0c30
authored
May 02, 2024
by
Jannik Streidl
Browse files
resolved conflicts #2
parents
dd52ea9d
5cf62139
Changes
81
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1404 additions
and
751 deletions
+1404
-751
src/lib/apis/auths/index.ts
src/lib/apis/auths/index.ts
+39
-0
src/lib/apis/rag/index.ts
src/lib/apis/rag/index.ts
+31
-0
src/lib/apis/streaming/index.ts
src/lib/apis/streaming/index.ts
+26
-28
src/lib/components/ChangelogModal.svelte
src/lib/components/ChangelogModal.svelte
+2
-2
src/lib/components/admin/AddUserModal.svelte
src/lib/components/admin/AddUserModal.svelte
+334
-0
src/lib/components/admin/SettingsModal.svelte
src/lib/components/admin/SettingsModal.svelte
+1
-2
src/lib/components/chat/MessageInput.svelte
src/lib/components/chat/MessageInput.svelte
+563
-464
src/lib/components/chat/MessageInput/Documents.svelte
src/lib/components/chat/MessageInput/Documents.svelte
+35
-1
src/lib/components/chat/MessageInput/Models.svelte
src/lib/components/chat/MessageInput/Models.svelte
+42
-36
src/lib/components/chat/MessageInput/Suggestions.svelte
src/lib/components/chat/MessageInput/Suggestions.svelte
+88
-28
src/lib/components/chat/Messages.svelte
src/lib/components/chat/Messages.svelte
+108
-103
src/lib/components/chat/Messages/CodeBlock.svelte
src/lib/components/chat/Messages/CodeBlock.svelte
+3
-1
src/lib/components/chat/Messages/Placeholder.svelte
src/lib/components/chat/Messages/Placeholder.svelte
+53
-25
src/lib/components/chat/Messages/ResponseMessage.svelte
src/lib/components/chat/Messages/ResponseMessage.svelte
+18
-17
src/lib/components/chat/ModelSelector.svelte
src/lib/components/chat/ModelSelector.svelte
+13
-9
src/lib/components/chat/ModelSelector/Selector.svelte
src/lib/components/chat/ModelSelector/Selector.svelte
+41
-23
src/lib/components/chat/SettingsModal.svelte
src/lib/components/chat/SettingsModal.svelte
+2
-3
src/lib/components/chat/ShareChatModal.svelte
src/lib/components/chat/ShareChatModal.svelte
+2
-3
src/lib/components/chat/ShortcutsModal.svelte
src/lib/components/chat/ShortcutsModal.svelte
+2
-3
src/lib/components/documents/AddDocModal.svelte
src/lib/components/documents/AddDocModal.svelte
+1
-3
No files found.
src/lib/apis/auths/index.ts
View file @
e57c0c30
...
@@ -95,6 +95,45 @@ export const userSignUp = async (
...
@@ -95,6 +95,45 @@ export const userSignUp = async (
return
res
;
return
res
;
};
};
export
const
addUser
=
async
(
token
:
string
,
name
:
string
,
email
:
string
,
password
:
string
,
role
:
string
=
'
pending
'
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/auths/add`
,
{
method
:
'
POST
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
...(
token
&&
{
authorization
:
`Bearer
${
token
}
`
})
},
body
:
JSON
.
stringify
({
name
:
name
,
email
:
email
,
password
:
password
,
role
:
role
})
})
.
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
;
};
export
const
updateUserProfile
=
async
(
token
:
string
,
name
:
string
,
profileImageUrl
:
string
)
=>
{
export
const
updateUserProfile
=
async
(
token
:
string
,
name
:
string
,
profileImageUrl
:
string
)
=>
{
let
error
=
null
;
let
error
=
null
;
...
...
src/lib/apis/rag/index.ts
View file @
e57c0c30
...
@@ -221,6 +221,37 @@ export const uploadWebToVectorDB = async (token: string, collection_name: string
...
@@ -221,6 +221,37 @@ export const uploadWebToVectorDB = async (token: string, collection_name: string
return
res
;
return
res
;
};
};
export
const
uploadYoutubeTranscriptionToVectorDB
=
async
(
token
:
string
,
url
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
RAG_API_BASE_URL
}
/youtube`
,
{
method
:
'
POST
'
,
headers
:
{
Accept
:
'
application/json
'
,
'
Content-Type
'
:
'
application/json
'
,
authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
url
:
url
})
})
.
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
queryDoc
=
async
(
export
const
queryDoc
=
async
(
token
:
string
,
token
:
string
,
collection_name
:
string
,
collection_name
:
string
,
...
...
src/lib/apis/streaming/index.ts
View file @
e57c0c30
import
{
EventSourceParserStream
}
from
'
eventsource-parser/stream
'
;
import
type
{
ParsedEvent
}
from
'
eventsource-parser
'
;
type
TextStreamUpdate
=
{
type
TextStreamUpdate
=
{
done
:
boolean
;
done
:
boolean
;
value
:
string
;
value
:
string
;
};
};
// createOpenAITextStream takes a
ReadableStreamDefaultReader from
a
n
SSE response,
// createOpenAITextStream takes a
responseBody with
a SSE response,
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
export
async
function
createOpenAITextStream
(
export
async
function
createOpenAITextStream
(
m
es
sageStream
:
ReadableStream
DefaultReader
,
r
es
ponseBody
:
ReadableStream
<
Uint8Array
>
,
splitLargeDeltas
:
boolean
splitLargeDeltas
:
boolean
):
Promise
<
AsyncGenerator
<
TextStreamUpdate
>>
{
):
Promise
<
AsyncGenerator
<
TextStreamUpdate
>>
{
let
iterator
=
openAIStreamToIterator
(
messageStream
);
const
eventStream
=
responseBody
.
pipeThrough
(
new
TextDecoderStream
())
.
pipeThrough
(
new
EventSourceParserStream
())
.
getReader
();
let
iterator
=
openAIStreamToIterator
(
eventStream
);
if
(
splitLargeDeltas
)
{
if
(
splitLargeDeltas
)
{
iterator
=
streamLargeDeltasAsRandomChunks
(
iterator
);
iterator
=
streamLargeDeltasAsRandomChunks
(
iterator
);
}
}
...
@@ -17,7 +24,7 @@ export async function createOpenAITextStream(
...
@@ -17,7 +24,7 @@ export async function createOpenAITextStream(
}
}
async
function
*
openAIStreamToIterator
(
async
function
*
openAIStreamToIterator
(
reader
:
ReadableStreamDefaultReader
reader
:
ReadableStreamDefaultReader
<
ParsedEvent
>
):
AsyncGenerator
<
TextStreamUpdate
>
{
):
AsyncGenerator
<
TextStreamUpdate
>
{
while
(
true
)
{
while
(
true
)
{
const
{
value
,
done
}
=
await
reader
.
read
();
const
{
value
,
done
}
=
await
reader
.
read
();
...
@@ -25,33 +32,24 @@ async function* openAIStreamToIterator(
...
@@ -25,33 +32,24 @@ async function* openAIStreamToIterator(
yield
{
done
:
true
,
value
:
''
};
yield
{
done
:
true
,
value
:
''
};
break
;
break
;
}
}
const
lines
=
value
.
split
(
'
\n
'
);
if
(
!
value
)
{
for
(
let
line
of
lines
)
{
continue
;
if
(
line
.
endsWith
(
'
\r
'
))
{
// Remove trailing \r
line
=
line
.
slice
(
0
,
-
1
);
}
}
if
(
line
!==
''
)
{
const
data
=
value
.
data
;
console
.
log
(
line
);
if
(
data
.
startsWith
(
'
[DONE]
'
))
{
if
(
line
===
'
data: [DONE]
'
)
{
yield
{
done
:
true
,
value
:
''
};
yield
{
done
:
true
,
value
:
''
};
}
else
if
(
line
.
startsWith
(
'
:
'
))
{
break
;
// Events starting with : are comments https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
}
// OpenRouter sends heartbeats like ": OPENROUTER PROCESSING"
continue
;
}
else
{
try
{
try
{
const
d
ata
=
JSON
.
parse
(
line
.
replace
(
/^data: /
,
''
)
);
const
parsedD
ata
=
JSON
.
parse
(
data
);
console
.
log
(
d
ata
);
console
.
log
(
parsedD
ata
);
yield
{
done
:
false
,
value
:
d
ata
.
choices
?.[
0
]?.
delta
?.
content
??
''
};
yield
{
done
:
false
,
value
:
parsedD
ata
.
choices
?.[
0
]?.
delta
?.
content
??
''
};
}
catch
(
e
)
{
}
catch
(
e
)
{
console
.
error
(
'
Error extracting delta from SSE event:
'
,
e
);
console
.
error
(
'
Error extracting delta from SSE event:
'
,
e
);
}
}
}
}
}
}
}
}
}
// streamLargeDeltasAsRandomChunks will chunk large deltas (length > 5) into random sized chunks between 1-3 characters
// streamLargeDeltasAsRandomChunks will chunk large deltas (length > 5) into random sized chunks between 1-3 characters
...
...
src/lib/components/ChangelogModal.svelte
View file @
e57c0c30
...
@@ -22,7 +22,7 @@
...
@@ -22,7 +22,7 @@
</script>
</script>
<Modal bind:show>
<Modal bind:show>
<div class="px-5 py-4 dark:text-gray-300">
<div class="px-5 py-4 dark:text-gray-300
text-gray-700
">
<div class="flex justify-between items-start">
<div class="flex justify-between items-start">
<div class="text-xl font-bold">
<div class="text-xl font-bold">
{$i18n.t('What’s New in')}
{$i18n.t('What’s New in')}
...
@@ -59,7 +59,7 @@
...
@@ -59,7 +59,7 @@
<hr class=" dark:border-gray-800" />
<hr class=" dark:border-gray-800" />
<div class=" w-full p-4 px-5">
<div class=" w-full p-4 px-5
text-gray-700 dark:text-gray-100
">
<div class=" overflow-y-scroll max-h-80">
<div class=" overflow-y-scroll max-h-80">
<div class="mb-3">
<div class="mb-3">
{#if changelog}
{#if changelog}
...
...
src/lib/components/admin/AddUserModal.svelte
0 → 100644
View file @
e57c0c30
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher } from 'svelte';
import { onMount, getContext } from 'svelte';
import { addUser } from '$lib/apis/auths';
import Modal from '../common/Modal.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let show = false;
let loading = false;
let tab = '';
let inputFiles;
let _user = {
name: '',
email: '',
password: '',
role: 'user'
};
$: if (show) {
_user = {
name: '',
email: '',
password: '',
role: 'user'
};
}
const submitHandler = async () => {
const stopLoading = () => {
dispatch('save');
loading = false;
};
if (tab === '') {
loading = true;
const res = await addUser(
localStorage.token,
_user.name,
_user.email,
_user.password,
_user.role
).catch((error) => {
toast.error(error);
});
if (res) {
stopLoading();
show = false;
}
} else {
if (inputFiles) {
loading = true;
const file = inputFiles[0];
const reader = new FileReader();
reader.onload = async (e) => {
const csv = e.target.result;
const rows = csv.split('\n');
let userCount = 0;
for (const [idx, row] of rows.entries()) {
const columns = row.split(',').map((col) => col.trim());
console.log(idx, columns);
if (idx > 0) {
if (columns.length === 4 && ['admin', 'user', 'pending'].includes(columns[3])) {
const res = await addUser(
localStorage.token,
columns[0],
columns[1],
columns[2],
columns[3]
).catch((error) => {
toast.error(`Row ${idx + 1}: ${error}`);
return null;
});
if (res) {
userCount = userCount + 1;
}
} else {
toast.error(`Row ${idx + 1}: invalid format.`);
}
}
}
toast.success(`Successfully imported ${userCount} users.`);
inputFiles = null;
const uploadInputElement = document.getElementById('upload-user-csv-input');
if (uploadInputElement) {
uploadInputElement.value = null;
}
stopLoading();
};
reader.readAsText(file);
} else {
toast.error(`File not found.`);
}
}
};
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Add 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>
<div class="flex flex-col md:flex-row w-full px-5 pb-4 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 text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2">
<button
class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
type="button"
on:click={() => {
tab = '';
}}>Form</button
>
<button
class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
type="button"
on:click={() => {
tab = 'import';
}}>CSV Import</button
>
</div>
<div class="px-1">
{#if tab === ''}
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
<div class="flex-1">
<select
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
bind:value={_user.role}
placeholder={$i18n.t('Enter Your Role')}
required
>
<option value="pending"> pending </option>
<option value="user"> user </option>
<option value="admin"> admin </option>
</select>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="text"
bind:value={_user.name}
placeholder={$i18n.t('Enter Your Full Name')}
autocomplete="off"
required
/>
</div>
</div>
<hr class=" dark:border-gray-800 my-3 w-full" />
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="email"
bind:value={_user.email}
placeholder={$i18n.t('Enter Your Email')}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="password"
bind:value={_user.password}
placeholder={$i18n.t('Enter Your Password')}
autocomplete="off"
/>
</div>
</div>
{:else if tab === 'import'}
<div>
<div class="mb-3 w-full">
<input
id="upload-user-csv-input"
hidden
bind:files={inputFiles}
type="file"
accept=".csv"
/>
<button
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
type="button"
on:click={() => {
document.getElementById('upload-user-csv-input')?.click();
}}
>
{#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{:else}
{$i18n.t('Click here to select a csv file.')}
{/if}
</button>
</div>
<div class=" text-xs text-gray-500">
ⓘ {$i18n.t(
'Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.'
)}
<a
class="underline dark:text-gray-200"
href="{WEBUI_BASE_URL}/static/user-import.csv"
>
Click here to download user import template file.
</a>
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading
? ' cursor-not-allowed'
: ''}"
type="submit"
disabled={loading}
>
{$i18n.t('Submit')}
{#if loading}
<div class="ml-2 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</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>
src/lib/components/admin/SettingsModal.svelte
View file @
e57c0c30
...
@@ -15,7 +15,7 @@
...
@@ -15,7 +15,7 @@
<Modal bind:show>
<Modal bind:show>
<div>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 p
y
-4">
<div class=" flex justify-between dark:text-gray-300 px-5 p
t
-4
pb-2
">
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
<button
<button
class="self-center"
class="self-center"
...
@@ -35,7 +35,6 @@
...
@@ -35,7 +35,6 @@
</svg>
</svg>
</button>
</button>
</div>
</div>
<hr class=" dark:border-gray-800" />
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
<div
<div
...
...
src/lib/components/chat/MessageInput.svelte
View file @
e57c0c30
<script lang="ts">
<script lang="ts">
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
import {
settings
} from '$lib/stores';
import {
modelfiles, settings, showSidebar
} from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import {
uploadDocToVectorDB,
uploadWebToVectorDB,
uploadYoutubeTranscriptionToVectorDB
} from '$lib/apis/rag';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS, WEBUI_BASE_URL } from '$lib/constants';
import { transcribeAudio } from '$lib/apis/audio';
import Prompts from './MessageInput/PromptCommands.svelte';
import Prompts from './MessageInput/PromptCommands.svelte';
import Suggestions from './MessageInput/Suggestions.svelte';
import Suggestions from './MessageInput/Suggestions.svelte';
import { uploadDocToVectorDB, uploadWebToVectorDB } from '$lib/apis/rag';
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
import Documents from './MessageInput/Documents.svelte';
import Documents from './MessageInput/Documents.svelte';
import Models from './MessageInput/Models.svelte';
import Models from './MessageInput/Models.svelte';
import { transcribeAudio } from '$lib/apis/audio';
import Tooltip from '../common/Tooltip.svelte';
import Tooltip from '../common/Tooltip.svelte';
import
Page
from '
../../../routes/(app)/+page
.svelte';
import
XMark
from '
$lib/components/icons/XMark
.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
export let submitPrompt: Function;
export let submitPrompt: Function;
export let stopResponse: Function;
export let stopResponse: Function;
export let suggestionPrompts = [];
export let autoScroll = true;
export let autoScroll = true;
export let selectedModel = '';
let chatTextAreaElement: HTMLTextAreaElement;
let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement;
let filesInputElement;
...
@@ -291,7 +298,36 @@
...
@@ -291,7 +298,36 @@
}
}
};
};
const uploadYoutubeTranscription = async (url) => {
console.log(url);
const doc = {
type: 'doc',
name: url,
collection_name: '',
upload_status: false,
url: url,
error: ''
};
try {
files = [...files, doc];
const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
if (res) {
doc.upload_status = true;
doc.collection_name = res.collection_name;
files = files;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(e);
}
};
onMount(() => {
onMount(() => {
console.log(document.getElementById('sidebar'));
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
const dropZone = document.querySelector('body');
const dropZone = document.querySelector('body');
...
@@ -390,12 +426,13 @@
...
@@ -390,12 +426,13 @@
</div>
</div>
{/if}
{/if}
<div class="w-full">
<div class="fixed bottom-0 {$showSidebar ? 'left-0 lg:left-[260px]' : 'left-0'} right-0">
<div class="px-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="w-full">
<div class="flex flex-col max-w-3xl w-full">
<div class="px-2.5 lg:px-16 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="flex flex-col max-w-5xl w-full">
<div class="relative">
<div class="relative">
{#if autoScroll === false && messages.length > 0}
{#if autoScroll === false && messages.length > 0}
<div class=" absolute -top-12 left-0 right-0 flex justify-center">
<div class=" absolute -top-12 left-0 right-0 flex justify-center
z-30
">
<button
<button
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
on:click={() => {
on:click={() => {
...
@@ -427,6 +464,10 @@
...
@@ -427,6 +464,10 @@
<Documents
<Documents
bind:this={documentsElement}
bind:this={documentsElement}
bind:prompt
bind:prompt
on:youtube={(e) => {
console.log(e);
uploadYoutubeTranscription(e.detail);
}}
on:url={(e) => {
on:url={(e) => {
console.log(e);
console.log(e);
uploadWeb(e.detail);
uploadWeb(e.detail);
...
@@ -443,24 +484,56 @@
...
@@ -443,24 +484,56 @@
];
];
}}
}}
/>
/>
{:else if prompt.charAt(0) === '@'}
{/if}
<Models
<Models
bind:this={modelsElement}
bind:this={modelsElement}
bind:prompt
bind:prompt
bind:user
bind:user
bind:chatInputPlaceholder
bind:chatInputPlaceholder
{messages}
{messages}
on:select={(e) => {
selectedModel = e.detail;
chatTextAreaElement?.focus();
}}
/>
/>
{/if}
{#if messages.length == 0 && suggestionPrompts.length !== 0}
{#if selectedModel !== ''}
<Suggestions {suggestionPrompts} {submitPrompt} />
<div
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900"
>
<div class="flex items-center gap-2 text-sm dark:text-gray-500">
<img
alt="model profile"
class="size-5 max-w-[28px] object-cover rounded-full"
src={$modelfiles.find((modelfile) => modelfile.tagName === selectedModel.id)
?.imageUrl ??
($i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
/>
<div>
Talking to <span class=" font-medium">{selectedModel.name} </span>
</div>
</div>
<div>
<button
class="flex items-center"
on:click={() => {
selectedModel = '';
}}
>
<XMark />
</button>
</div>
</div>
{/if}
{/if}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-900">
<div class="bg-white dark:bg-gray-900">
<div class="max-w-
3
xl px-2.5 mx-auto inset-x-0">
<div class="max-w-
6
xl px-2.5
lg:px-16
mx-auto inset-x-0">
<div class=" pb-2">
<div class=" pb-2">
<input
<input
bind:this={filesInputElement}
bind:this={filesInputElement}
...
@@ -679,7 +752,7 @@
...
@@ -679,7 +752,7 @@
<textarea
<textarea
id="chat-textarea"
id="chat-textarea"
bind:this={chatTextAreaElement}
bind:this={chatTextAreaElement}
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
class="
scrollbar-none
dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
? ''
? ''
: ' pl-4'} rounded-xl resize-none h-[48px]"
: ' pl-4'} rounded-xl resize-none h-[48px]"
placeholder={chatInputPlaceholder !== ''
placeholder={chatInputPlaceholder !== ''
...
@@ -689,7 +762,13 @@
...
@@ -689,7 +762,13 @@
: $i18n.t('Send a Message')}
: $i18n.t('Send a Message')}
bind:value={prompt}
bind:value={prompt}
on:keypress={(e) => {
on:keypress={(e) => {
if (window.innerWidth > 1024) {
if (
!(
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
)
) {
if (e.keyCode == 13 && !e.shiftKey) {
if (e.keyCode == 13 && !e.shiftKey) {
e.preventDefault();
e.preventDefault();
}
}
...
@@ -791,6 +870,14 @@
...
@@ -791,6 +870,14 @@
e.preventDefault();
e.preventDefault();
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
}
e.target.style.height = '';
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}
if (e.key === 'Escape') {
console.log('Escape');
selectedModel = '';
}
}
}}
}}
rows="1"
rows="1"
...
@@ -952,4 +1039,16 @@
...
@@ -952,4 +1039,16 @@
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.scrollbar-none:active::-webkit-scrollbar-thumb,
.scrollbar-none:focus::-webkit-scrollbar-thumb,
.scrollbar-none:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.scrollbar-none::-webkit-scrollbar-thumb {
visibility: hidden;
}
</style>
src/lib/components/chat/MessageInput/Documents.svelte
View file @
e57c0c30
...
@@ -87,6 +87,17 @@
...
@@ -87,6 +87,17 @@
chatInputElement?.focus();
chatInputElement?.focus();
await tick();
await tick();
};
};
const confirmSelectYoutube = async (url) => {
dispatch('youtube', url);
prompt = removeFirstHashWord(prompt);
const chatInputElement = document.getElementById('chat-textarea');
await tick();
chatInputElement?.focus();
await tick();
};
</script>
</script>
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
...
@@ -132,7 +143,30 @@
...
@@ -132,7 +143,30 @@
</button>
</button>
{/each}
{/each}
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('https://www.youtube.com')}
<button
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
type="button"
on:click={() => {
const url = prompt.split(' ')?.at(0)?.substring(1);
if (isValidHttpUrl(url)) {
confirmSelectYoutube(url);
} else {
toast.error(
$i18n.t(
'Oops! Looks like the URL is invalid. Please double-check and try again.'
)
);
}
}}
>
<div class=" font-medium text-black line-clamp-1">
{prompt.split(' ')?.at(0)?.substring(1)}
</div>
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
</button>
{:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<button
<button
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
type="button"
type="button"
...
...
src/lib/components/chat/MessageInput/Models.svelte
View file @
e57c0c30
<script lang="ts">
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { generatePrompt } from '$lib/apis/ollama';
import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores';
import { models } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { splitStream } from '$lib/utils';
...
@@ -7,6 +9,8 @@
...
@@ -7,6 +9,8 @@
const i18n = getContext('i18n');
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let prompt = '';
export let prompt = '';
export let user = null;
export let user = null;
...
@@ -17,12 +21,7 @@
...
@@ -17,12 +21,7 @@
let filteredModels = [];
let filteredModels = [];
$: filteredModels = $models
$: filteredModels = $models
.filter(
.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
(p) =>
p.name !== 'hr' &&
!p.external &&
p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? '')
)
.sort((a, b) => a.name.localeCompare(b.name));
.sort((a, b) => a.name.localeCompare(b.name));
$: if (prompt) {
$: if (prompt) {
...
@@ -38,6 +37,11 @@
...
@@ -38,6 +37,11 @@
};
};
const confirmSelect = async (model) => {
const confirmSelect = async (model) => {
prompt = '';
dispatch('select', model);
};
const confirmSelectCollaborativeChat = async (model) => {
// dispatch('select', model);
// dispatch('select', model);
prompt = '';
prompt = '';
user = JSON.parse(JSON.stringify(model.name));
user = JSON.parse(JSON.stringify(model.name));
...
@@ -127,7 +131,8 @@
...
@@ -127,7 +131,8 @@
};
};
</script>
</script>
{#if filteredModels.length > 0}
{#if prompt.charAt(0) === '@'}
{#if filteredModels.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2">
<div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
...
@@ -163,4 +168,5 @@
...
@@ -163,4 +168,5 @@
</div>
</div>
</div>
</div>
</div>
</div>
{/if}
{/if}
{/if}
src/lib/components/chat/MessageInput/Suggestions.svelte
View file @
e57c0c30
<script lang="ts">
<script lang="ts">
import Bolt from '$lib/components/icons/Bolt.svelte';
import { onMount } from 'svelte';
export let submitPrompt: Function;
export let submitPrompt: Function;
export let suggestionPrompts = [];
export let suggestionPrompts = [];
let prompts = [];
let prompts = [];
$: prompts =
$: prompts = suggestionPrompts
suggestionPrompts.length <= 4
.reduce((acc, current) => [...acc, ...[current]], [])
? suggestionPrompts
.sort(() => Math.random() - 0.5);
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
// suggestionPrompts.length <= 4
// ? suggestionPrompts
// : suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
onMount(() => {
const containerElement = document.getElementById('suggestions-container');
if (containerElement) {
containerElement.addEventListener('wheel', function (event) {
if (event.deltaY !== 0) {
// If scrolling vertically, prevent default behavior
event.preventDefault();
// Adjust horizontal scroll position based on vertical scroll
containerElement.scrollLeft += event.deltaY;
}
});
}
});
</script>
</script>
<div class=" mb-3 md:p-1 text-left w-full">
{#if prompts.length > 0}
<div class=" flex flex-wrap-reverse px-2 text-left">
<div class="mb-2 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600">
{#each prompts as prompt, promptIdx}
<Bolt />
Suggested
</div>
{/if}
<div class="w-full">
<div
<div
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
class="relative w-full flex gap-2 snap-x snap-mandatory md:snap-none overflow-x-auto tabs"
id="suggestions-container"
>
>
{#each prompts as prompt, promptIdx}
<div class="snap-center shrink-0">
<button
<button
class=" flex-
1
flex justify-between
w-full h-full px-4 py-2.5
bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-
2
xl transition group"
class="
flex
flex-
col
flex
-1 shrink-0 w-64
justify-between
h-36 p-5 px-6
bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-
3
xl transition group"
on:click={() => {
on:click={() => {
submitPrompt(prompt.content);
submitPrompt(prompt.content);
}}
}}
>
>
<div class="flex flex-col text-left
self-center
">
<div class="flex flex-col text-left">
{#if prompt.title && prompt.title[0] !== ''}
{#if prompt.title && prompt.title[0] !== ''}
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
<div
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
class=" font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition"
>
{prompt.title[0]}
</div>
<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
{:else}
{:else}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
<div
class=" self-center text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
>
{prompt.content}
{prompt.content}
</div>
</div>
{/if}
{/if}
</div>
</div>
<div class="w-full flex justify-between">
<div
class="text-xs text-gray-400 group-hover:text-gray-500 dark:text-gray-600 dark:group-hover:text-gray-500 transition self-center"
>
Prompt
</div>
<div
<div
class="self-
c
en
ter
p-1 rounded-lg text-gray-
5
0 group-hover:text-gray-800 dark:text-gray-
85
0 dark:group-hover:text-gray-100 transition"
class="self-en
d
p-1 rounded-lg text-gray-
30
0 group-hover:text-gray-800 dark:text-gray-
70
0 dark:group-hover:text-gray-100 transition"
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
viewBox="0 0 16 16"
fill="currentColor"
fill="currentColor"
class="
w-4 h
-4"
class="
size
-4"
>
>
<path
<path
fill-rule="evenodd"
fill-rule="evenodd"
...
@@ -49,8 +90,27 @@
...
@@ -49,8 +90,27 @@
/>
/>
</svg>
</svg>
</div>
</div>
</div>
</button>
</button>
</div>
</div>
{/each}
{/each}
<!-- <div class="snap-center shrink-0">
<img
class="shrink-0 w-80 h-40 rounded-lg shadow-xl bg-white"
src="https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80"
/>
</div> -->
</div>
</div>
</div>
</div>
<style>
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
src/lib/components/chat/Messages.svelte
View file @
e57c0c30
...
@@ -12,7 +12,7 @@
...
@@ -12,7 +12,7 @@
import Placeholder from './Messages/Placeholder.svelte';
import Placeholder from './Messages/Placeholder.svelte';
import Spinner from '../common/Spinner.svelte';
import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images';
import { imageGenerations } from '$lib/apis/images';
import { copyToClipboard } from '$lib/utils';
import { copyToClipboard
, findWordIndices
} from '$lib/utils';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
...
@@ -22,6 +22,8 @@
...
@@ -22,6 +22,8 @@
export let continueGeneration: Function;
export let continueGeneration: Function;
export let regenerateResponse: Function;
export let regenerateResponse: Function;
export let prompt;
export let suggestionPrompts;
export let processing = '';
export let processing = '';
export let bottomPadding = false;
export let bottomPadding = false;
export let autoScroll;
export let autoScroll;
...
@@ -236,56 +238,58 @@
...
@@ -236,56 +238,58 @@
history: history
history: history
});
});
};
};
// const messageDeleteHandler = async (messageId) => {
// const message = history.messages[messageId];
// const parentId = message.parentId;
// const childrenIds = message.childrenIds ?? [];
// const grandchildrenIds = [];
// // Iterate through childrenIds to find grandchildrenIds
// for (const childId of childrenIds) {
// const childMessage = history.messages[childId];
// const grandChildrenIds = childMessage.childrenIds ?? [];
// for (const grandchildId of grandchildrenIds) {
// const childMessage = history.messages[grandchildId];
// childMessage.parentId = parentId;
// }
// grandchildrenIds.push(...grandChildrenIds);
// }
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
// (id) => id !== messageId
// );
// // Select latest message
// let currentMessageId = grandchildrenIds.at(-1);
// if (currentMessageId) {
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
// while (messageChildrenIds.length !== 0) {
// currentMessageId = messageChildrenIds.at(-1);
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
// }
// history.currentId = currentMessageId;
// }
// await updateChatById(localStorage.token, chatId, { messages, history });
// };
</script>
</script>
{#if messages.length == 0}
<div class="h-full flex mb-16">
<Placeholder models={selectedModels} modelfiles={selectedModelfiles} />
{#if messages.length == 0}
{:else}
<Placeholder
<div class=" pb-10">
models={selectedModels}
modelfiles={selectedModelfiles}
{suggestionPrompts}
submitPrompt={async (p) => {
let text = p;
if (p.includes('{{CLIPBOARD}}')) {
const clipboardText = await navigator.clipboard.readText().catch((err) => {
toast.error($i18n.t('Failed to read clipboard contents'));
return '{{CLIPBOARD}}';
});
text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
}
prompt = text;
await tick();
const chatInputElement = document.getElementById('chat-textarea');
if (chatInputElement) {
prompt = p;
chatInputElement.style.height = '';
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
chatInputElement.focus();
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
}
await tick();
}}
/>
{:else}
<div class="w-full pt-2">
{#key chatId}
{#key chatId}
{#each messages as message, messageIdx}
{#each messages as message, messageIdx}
<div class=" w-full">
<div class=" w-full
{messageIdx === messages.length - 1 ? 'pb-28' : ''}
">
<div
<div
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
? 'max-w-full'
? 'max-w-full'
: 'max-w-
3
xl'} mx-auto rounded-lg group"
: 'max-w-
5
xl'} mx-auto rounded-lg group"
>
>
{#if message.role === 'user'}
{#if message.role === 'user'}
<UserMessage
<UserMessage
...
@@ -336,8 +340,9 @@
...
@@ -336,8 +340,9 @@
{/each}
{/each}
{#if bottomPadding}
{#if bottomPadding}
<div class="
m
b-
1
0" />
<div class="
p
b-
2
0" />
{/if}
{/if}
{/key}
{/key}
</div>
</div>
{/if}
{/if}
</div>
src/lib/components/chat/Messages/CodeBlock.svelte
View file @
e57c0c30
...
@@ -31,7 +31,9 @@
...
@@ -31,7 +31,9 @@
>
>
</div>
</div>
<pre class=" rounded-b-lg hljs p-4 px-5 overflow-x-auto rounded-t-none"><code
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px;"><code
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
></pre>
></pre>
</div>
</div>
...
...
src/lib/components/chat/Messages/Placeholder.svelte
View file @
e57c0c30
...
@@ -3,11 +3,19 @@
...
@@ -3,11 +3,19 @@
import { user } from '$lib/stores';
import { user } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { onMount, getContext } from 'svelte';
import { blur, fade } from 'svelte/transition';
import Suggestions from '../MessageInput/Suggestions.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
export let models = [];
export let models = [];
export let modelfiles = [];
export let modelfiles = [];
export let submitPrompt;
export let suggestionPrompts;
let mounted = false;
let modelfile = null;
let modelfile = null;
let selectedModelIdx = 0;
let selectedModelIdx = 0;
...
@@ -17,12 +25,16 @@
...
@@ -17,12 +25,16 @@
$: if (models.length > 0) {
$: if (models.length > 0) {
selectedModelIdx = models.length - 1;
selectedModelIdx = models.length - 1;
}
}
onMount(() => {
mounted = true;
});
</script>
</script>
{#
if models.length > 0
}
{#
key mounted
}
<div class="m-auto
text-center
max-w-
md
px-
2
">
<div class="m-auto
w-full
max-w-
6xl
px-
8 lg:px-24 pb-16
">
<div class="flex justify-
center mt-8
">
<div class="flex justify-
start
">
<div class="flex -space-x-4 mb-1">
<div class="flex -space-x-4 mb-1"
in:fade={{ duration: 200 }}
>
{#each models as model, modelIdx}
{#each models as model, modelIdx}
<button
<button
on:click={() => {
on:click={() => {
...
@@ -33,7 +45,7 @@
...
@@ -33,7 +45,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=" size-
12
rounded-full border-[1px] border-gray-200 dark:border-none"
class=" size-
[2.7rem]
rounded-full border-[1px] border-gray-200 dark:border-none"
draggable="false"
draggable="false"
/>
/>
{:else}
{:else}
...
@@ -41,7 +53,7 @@
...
@@ -41,7 +53,7 @@
src={$i18n.language === 'dg-DG'
src={$i18n.language === 'dg-DG'
? `/doge.png`
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`}
: `${WEBUI_BASE_URL}/static/favicon.png`}
class=" size-
12
rounded-full border-[1px] border-gray-200 dark:border-none"
class=" size-
[2.7rem]
rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
alt="logo"
draggable="false"
draggable="false"
/>
/>
...
@@ -50,26 +62,42 @@
...
@@ -50,26 +62,42 @@
{/each}
{/each}
</div>
</div>
</div>
</div>
<div class=" mt-2 mb-5 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
<div
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4"
>
<div>
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
{#if modelfile}
{#if modelfile}
<span class=" capitalize">
{modelfile.title}
{modelfile.title}
</span>
{:else}
<div class="mt-0.5 text-base font-normal text-gray-600 dark:text-gray-400">
{$i18n.t('Hello, {{name}}', { name: $user.name })}
{/if}
</div>
<div in:fade={{ duration: 200, delay: 200 }}>
{#if modelfile}
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400">
{modelfile.desc}
{modelfile.desc}
</div>
</div>
{#if modelfile.user}
{#if modelfile.user}
<div class="mt-0.5 text-sm font-normal text-gray-
5
00 dark:text-gray-500">
<div class="mt-0.5 text-sm font-normal text-gray-
4
00 dark:text-gray-500">
By <a href="https://openwebui.com/m/{modelfile.user.username}"
By <a href="https://openwebui.com/m/{modelfile.user.username}"
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
>
>
</div>
</div>
{/if}
{/if}
{:else}
{:else}
<div class="
line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div
>
<div class="
font-medium text-gray-400 dark:text-gray-500"
>
{$i18n.t('How can I help you today?')}
<div>{$i18n.t('How can I help you today?')}
</div>
</div>
{/if}
{/if}
</div>
</div>
</div>
</div>
{/if}
</div>
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
<Suggestions {suggestionPrompts} {submitPrompt} />
</div>
</div>
{/key}
src/lib/components/chat/Messages/ResponseMessage.svelte
View file @
e57c0c30
...
@@ -137,6 +137,7 @@
...
@@ -137,6 +137,7 @@
.getElementById(`message-${message.id}`)
.getElementById(`message-${message.id}`)
?.getElementsByClassName('chat-assistant');
?.getElementsByClassName('chat-assistant');
if (chatMessageElements) {
for (const element of chatMessageElements) {
for (const element of chatMessageElements) {
auto_render(element, {
auto_render(element, {
// customised options
// customised options
...
@@ -152,6 +153,7 @@
...
@@ -152,6 +153,7 @@
throwOnError: false
throwOnError: false
});
});
}
}
}
};
};
const playAudio = (idx) => {
const playAudio = (idx) => {
...
@@ -325,9 +327,8 @@
...
@@ -325,9 +327,8 @@
{#key message.id}
{#key message.id}
<div class=" flex w-full message-{message.id}" id="message-{message.id}">
<div class=" flex w-full message-{message.id}" id="message-{message.id}">
<ProfileImage
<ProfileImage
src={modelfiles[message.model]?.imageUrl ?? $i18n.language === 'dg-DG'
src={modelfiles[message.model]?.imageUrl ??
? `/doge.png`
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
: `${WEBUI_BASE_URL}/static/favicon.png`}
/>
/>
<div class="w-full overflow-hidden">
<div class="w-full overflow-hidden">
...
...
src/lib/components/chat/ModelSelector.svelte
View file @
e57c0c30
...
@@ -13,6 +13,8 @@
...
@@ -13,6 +13,8 @@
export let selectedModels = [''];
export let selectedModels = [''];
export let disabled = false;
export let disabled = false;
export let showSetDefault = true;
const saveDefaultModel = async () => {
const saveDefaultModel = async () => {
const hasEmptyModel = selectedModels.filter((it) => it === '');
const hasEmptyModel = selectedModels.filter((it) => it === '');
if (hasEmptyModel.length) {
if (hasEmptyModel.length) {
...
@@ -38,9 +40,9 @@
...
@@ -38,9 +40,9 @@
<div class="flex flex-col mt-0.5 w-full">
<div class="flex flex-col mt-0.5 w-full">
{#each selectedModels as selectedModel, selectedModelIdx}
{#each selectedModels as selectedModel, selectedModelIdx}
<div class="flex w-full">
<div class="flex w-full
max-w-fit
">
<div class="overflow-hidden w-full">
<div class="overflow-hidden w-full">
<div class="mr-
0.5
max-w-full">
<div class="mr-
1
max-w-full">
<Selector
<Selector
placeholder={$i18n.t('Select a model')}
placeholder={$i18n.t('Select a model')}
items={$models
items={$models
...
@@ -69,9 +71,9 @@
...
@@ -69,9 +71,9 @@
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="
size-3.5
"
>
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg>
</svg>
...
@@ -92,9 +94,9 @@
...
@@ -92,9 +94,9 @@
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="
size-3.5
"
>
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
</svg>
...
@@ -106,6 +108,8 @@
...
@@ -106,6 +108,8 @@
{/each}
{/each}
</div>
</div>
<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
{#if showSetDefault}
<div class="text-left mt-0.5 ml-1 text-[0.7rem] 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>
{/if}
src/lib/components/chat/ModelSelector/Selector.svelte
View file @
e57c0c30
<script lang="ts">
<script lang="ts">
import {
Select
} from 'bits-ui';
import {
DropdownMenu
} from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { flyAndScale } from '$lib/utils/transitions';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
...
@@ -25,6 +25,11 @@
...
@@ -25,6 +25,11 @@
export let items = [{ value: 'mango', label: 'Mango' }];
export let items = [{ value: 'mango', label: 'Mango' }];
export let className = 'max-w-lg';
let selectedModel = '';
$: selectedModel = items.find((item) => item.value === value) ?? '';
let searchValue = '';
let searchValue = '';
let ollamaVersion = null;
let ollamaVersion = null;
...
@@ -175,27 +180,28 @@
...
@@ -175,27 +180,28 @@
};
};
</script>
</script>
<Select.Root
<DropdownMenu.Root
{items}
onOpenChange={async () => {
onOpenChange={async () => {
searchValue = '';
searchValue = '';
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}}
}}
selected={items.find((item) => item.value === value) ?? ''}
onSelectedChange={(selectedItem) => {
value = selectedItem.value;
}}
>
>
<Select.Trigger class="relative w-full" aria-label={placeholder}>
<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}>
<Select.Value
<div
class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
>
{#if selectedModel}
{selectedModel.label}
{:else}
{placeholder}
{placeholder}
/>
{/if}
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
</Select.Trigger>
</div>
<Select.Content
</DropdownMenu.Trigger>
class=" z-40 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"
<DropdownMenu.Content
class=" z-40 w-full {className} justify-start 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}
transition={flyAndScale}
side={'bottom-start'}
sideOffset={4}
sideOffset={4}
>
>
<slot>
<slot>
...
@@ -214,12 +220,13 @@
...
@@ -214,12 +220,13 @@
<hr class="border-gray-100 dark:border-gray-800" />
<hr class="border-gray-100 dark:border-gray-800" />
{/if}
{/if}
<div class="px-3 my-2 max-h-72 overflow-y-auto">
<div class="px-3 my-2 max-h-72 overflow-y-auto
scrollbar-none
">
{#each filteredItems as item}
{#each filteredItems as item}
<
Select
.Item
<
DropdownMenu
.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 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
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 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
value={item.value}
on:click={() => {
label={item.label}
value = item.value;
}}
>
>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2">
<div class="line-clamp-1">
<div class="line-clamp-1">
...
@@ -287,7 +294,7 @@
...
@@ -287,7 +294,7 @@
<Check />
<Check />
</div>
</div>
{/if}
{/if}
</
Select
.Item>
</
DropdownMenu
.Item>
{:else}
{:else}
<div>
<div>
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
...
@@ -386,5 +393,16 @@
...
@@ -386,5 +393,16 @@
{/each}
{/each}
</div>
</div>
</slot>
</slot>
</Select.Content>
</DropdownMenu.Content>
</Select.Root>
</DropdownMenu.Root>
<style>
.scrollbar-none:active::-webkit-scrollbar-thumb,
.scrollbar-none:focus::-webkit-scrollbar-thumb,
.scrollbar-none:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.scrollbar-none::-webkit-scrollbar-thumb {
visibility: hidden;
}
</style>
src/lib/components/chat/SettingsModal.svelte
View file @
e57c0c30
...
@@ -35,8 +35,8 @@
...
@@ -35,8 +35,8 @@
</script>
</script>
<Modal bind:show>
<Modal bind:show>
<div>
<div
class="text-gray-700 dark:text-gray-100"
>
<div class=" flex justify-between dark:text-gray-300 px-5 p
y
-4">
<div class=" flex justify-between dark:text-gray-300 px-5 p
t
-4
pb-1
">
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
<button
<button
class="self-center"
class="self-center"
...
@@ -56,7 +56,6 @@
...
@@ -56,7 +56,6 @@
</svg>
</svg>
</button>
</button>
</div>
</div>
<hr class=" dark:border-gray-800" />
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
<div
<div
...
...
src/lib/components/chat/ShareChatModal.svelte
View file @
e57c0c30
...
@@ -71,7 +71,7 @@
...
@@ -71,7 +71,7 @@
<Modal bind:show size="sm">
<Modal bind:show size="sm">
<div>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 p
y
-4">
<div class=" flex justify-between dark:text-gray-300 px-5 p
t
-4
pb-0.5
">
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div>
<button
<button
class="self-center"
class="self-center"
...
@@ -91,10 +91,9 @@
...
@@ -91,10 +91,9 @@
</svg>
</svg>
</button>
</button>
</div>
</div>
<hr class=" dark:border-gray-800" />
{#if chat}
{#if chat}
<div class="px-
4
pt-4 pb-5 w-full flex flex-col justify-center">
<div class="px-
5
pt-4 pb-5 w-full flex flex-col justify-center">
<div class=" text-sm dark:text-gray-300 mb-1">
<div class=" text-sm dark:text-gray-300 mb-1">
{#if chat.share_id}
{#if chat.share_id}
<a href="/s/{chat.share_id}" target="_blank"
<a href="/s/{chat.share_id}" target="_blank"
...
...
src/lib/components/chat/ShortcutsModal.svelte
View file @
e57c0c30
...
@@ -8,8 +8,8 @@
...
@@ -8,8 +8,8 @@
</script>
</script>
<Modal bind:show>
<Modal bind:show>
<div>
<div
class="text-gray-700 dark:text-gray-100"
>
<div class=" flex justify-between dark:text-gray-300 px-5 p
y
-4">
<div class=" flex justify-between dark:text-gray-300 px-5 p
t
-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
<button
<button
class="self-center"
class="self-center"
...
@@ -29,7 +29,6 @@
...
@@ -29,7 +29,6 @@
</svg>
</svg>
</button>
</button>
</div>
</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 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">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
...
...
src/lib/components/documents/AddDocModal.svelte
View file @
e57c0c30
...
@@ -96,7 +96,7 @@
...
@@ -96,7 +96,7 @@
<Modal size="sm" bind:show>
<Modal size="sm" bind:show>
<div>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 p
y
-4">
<div class=" flex justify-between dark:text-gray-300 px-5 p
t
-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div>
<button
<button
class="self-center"
class="self-center"
...
@@ -116,8 +116,6 @@
...
@@ -116,8 +116,6 @@
</svg>
</svg>
</button>
</button>
</div>
</div>
<hr class=" dark:border-gray-800" />
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class="flex flex-col md:flex-row w-full px-5 py-4 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">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
<form
...
...
Prev
1
2
3
4
5
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